diff --git a/.eslintrc b/.eslintrc index a656e80d38..7898fbf829 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,96 +1,3 @@ { - "env": { - "browser": true, - "builtin": true, - "es6": true, - "jasmine": true, - "mocha": true, - "node": true - }, - "parserOptions": { - "ecmaVersion": 7, - "sourceType": "module" - }, - "globals": { - "$": true, - "_": true, - "andThen": true, - "asyncRender": true, - "Blob": true, - "bootbox": true, - "click": true, - "waitUntil": true, - "getSettledState": true, - "count": true, - "currentPath": true, - "currentRouteName": true, - "currentURL": true, - "define": true, - "Discourse": true, - "Ember": true, - "exists": true, - "File": true, - "fillIn": true, - "find": true, - "Handlebars": true, - "hasModule": true, - "I18n": true, - "invisible": true, - "jQuery": true, - "keyboardHelper": true, - "keyEvent": true, - "moduleFor": true, - "moduleForComponent": true, - "moment": true, - "Pretender": true, - "QUnit": true, - "require": true, - "requirejs": true, - "RSVP": true, - "sandbox": true, - "sinon": true, - "test": true, - "triggerEvent": true, - "visible": true, - "visit": true, - "pauseTest": true - }, - "rules": { - "block-scoped-var": 2, - "dot-notation": 0, - "eqeqeq": [2, "allow-null"], - "guard-for-in": 2, - "no-alert": 2, - "no-bitwise": 2, - "no-caller": 2, - "no-cond-assign": 0, - "no-console": 2, - "no-debugger": 2, - "no-empty": 0, - "no-eval": 2, - "no-extend-native": 2, - "no-extra-parens": 0, - "no-inner-declarations": 2, - "no-irregular-whitespace": 2, - "no-iterator": 2, - "no-loop-func": 2, - "no-mixed-spaces-and-tabs": 2, - "no-multi-str": 2, - "no-new": 2, - "no-plusplus": 0, - "no-proto": 2, - "no-script-url": 2, - "no-sequences": 2, - "no-shadow": 2, - "no-this-before-super": 2, - "no-trailing-spaces": 2, - "no-undef": 2, - "no-unused-vars": 2, - "no-with": 2, - "semi": 2, - "strict": 0, - "valid-typeof": 2, - "wrap-iife": [2, "inside"] - }, - "parser": "babel-eslint" + "extends": "eslint-config-discourse" } diff --git a/Gemfile b/Gemfile index 98e165f3ac..fe9faf36a4 100644 --- a/Gemfile +++ b/Gemfile @@ -26,6 +26,10 @@ else gem 'sprockets-rails' end +# this will eventually be added to rails, +# allows us to precompile all our templates in the unicorn master +gem 'actionview_precompiler', require: false + gem 'seed-fu' gem 'mail', require: false @@ -46,7 +50,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.9.13' +gem 'onebox', '1.9.17' gem 'http_accept_language', '~>2.0.5', require: false @@ -114,6 +118,8 @@ gem 'execjs', require: false gem 'mini_racer' gem 'highline', '~> 1.7.0', require: false gem 'rack-protection' # security +gem 'cbor', require: false +gem 'cose', require: false # Gems used only for assets and not required in production environments by default. # Allow everywhere for now cause we are allowing asset debugging in production diff --git a/Gemfile.lock b/Gemfile.lock index e859be90ef..89de47af23 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -20,6 +20,8 @@ GEM erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) + actionview_precompiler (0.2.1) + actionview (>= 6.0.a) active_model_serializers (0.8.4) activemodel (>= 3.0) activejob (6.0.0) @@ -77,12 +79,15 @@ GEM activesupport (>= 3.0.0) uniform_notifier (~> 1.11) byebug (11.0.1) + cbor (0.5.9.6) certified (1.0.0) chunky_png (1.3.11) coderay (1.1.2) colored2 (3.1.2) concurrent-ruby (1.1.5) connection_pool (2.2.2) + cose (0.9.0) + cbor (~> 0.5.9) cppjieba_rb (0.3.3) crack (0.4.3) safe_yaml (~> 1.0.0) @@ -92,7 +97,7 @@ GEM debug_inspector (0.0.3) diff-lcs (1.3) diffy (3.3.0) - discourse-ember-source (3.10.0.1) + discourse-ember-source (3.10.0.2) discourse_image_optim (0.26.2) exifr (~> 1.2, >= 1.2.2) fspath (~> 3.0) @@ -168,7 +173,7 @@ GEM logstash-event (1.2.02) logstash-logger (0.26.1) logstash-event (~> 1.2) - logster (2.3.2) + logster (2.3.3) loofah (2.2.3) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -240,7 +245,7 @@ GEM omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack - onebox (1.9.13) + onebox (1.9.17) htmlentities (~> 4.3) moneta (~> 1.0) multi_json (~> 1.11) @@ -355,7 +360,7 @@ GEM ruby_dep (1.5.0) rubyzip (2.0.0) safe_yaml (1.0.5) - sanitize (5.0.0) + sanitize (5.1.0) crass (~> 1.0.2) nokogiri (>= 1.8.0) nokogumbo (~> 2.0) @@ -425,6 +430,7 @@ DEPENDENCIES actionmailer (= 6.0.0) actionpack (= 6.0.0) actionview (= 6.0.0) + actionview_precompiler active_model_serializers (~> 0.8.3) activemodel (= 6.0.0) activerecord (= 6.0.0) @@ -438,8 +444,10 @@ DEPENDENCIES bootsnap bullet byebug + cbor certified colored2 + cose cppjieba_rb css_parser diffy @@ -493,7 +501,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.9.13) + onebox (= 1.9.17) openid-redis-store parallel_tests pg diff --git a/app/assets/javascripts/admin-login/admin-login.js.es6 b/app/assets/javascripts/admin-login/admin-login.js.es6 new file mode 100644 index 0000000000..e7404b386a --- /dev/null +++ b/app/assets/javascripts/admin-login/admin-login.js.es6 @@ -0,0 +1,46 @@ +import { getWebauthnCredential } from "discourse/lib/webauthn"; + +export default function() { + document.getElementById( + "activate-security-key-alternative" + ).onclick = function() { + document.getElementById("second-factor-forms").style.display = "block"; + document.getElementById("primary-security-key-form").style.display = "none"; + }; + + document.getElementById("submit-security-key").onclick = function(e) { + e.preventDefault(); + getWebauthnCredential( + document.getElementById("security-key-challenge").value, + document + .getElementById("security-key-allowed-credential-ids") + .value.split(","), + credentialData => { + document.getElementById( + "security-key-credential" + ).value = JSON.stringify(credentialData); + e.target.parentElement.submit(); + }, + errorMessage => { + document.getElementById("security-key-error").innerText = errorMessage; + } + ); + }; + + const useTotp = I18n.t("login.second_factor_toggle.totp"); + const useBackup = I18n.t("login.second_factor_toggle.backup_code"); + const backupForm = document.getElementById("backup-second-factor-form"); + const primaryForm = document.getElementById("primary-second-factor-form"); + document.getElementById("toggle-form").onclick = function(event) { + event.preventDefault(); + if (backupForm.style.display === "none") { + backupForm.style.display = "block"; + primaryForm.style.display = "none"; + document.getElementById("toggle-form").innerHTML = useTotp; + } else { + backupForm.style.display = "none"; + primaryForm.style.display = "block"; + document.getElementById("toggle-form").innerHTML = useBackup; + } + }; +} diff --git a/app/assets/javascripts/admin-login/admin-login.no-module.js.es6 b/app/assets/javascripts/admin-login/admin-login.no-module.js.es6 new file mode 100644 index 0000000000..4de268433d --- /dev/null +++ b/app/assets/javascripts/admin-login/admin-login.no-module.js.es6 @@ -0,0 +1 @@ +require("admin-login/admin-login").default(); diff --git a/app/assets/javascripts/admin/components/admin-report-stacked-chart.js.es6 b/app/assets/javascripts/admin/components/admin-report-stacked-chart.js.es6 index ce0dfd9021..852bc92b3d 100644 --- a/app/assets/javascripts/admin/components/admin-report-stacked-chart.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report-stacked-chart.js.es6 @@ -33,6 +33,10 @@ export default Ember.Component.extend({ _scheduleChartRendering() { Ember.run.schedule("afterRender", () => { + if (!this.element) { + return; + } + this._renderChart( this.model, this.element.querySelector(".chart-canvas") diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 index bd830be687..8a8ba62481 100644 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ b/app/assets/javascripts/admin/models/admin-user.js.es6 @@ -243,12 +243,12 @@ const AdminUser = Discourse.User.extend({ this.set("originalTrustLevel", this.trust_level); }, - dirty: propertyNotEqual("originalTrustLevel", "trustLevel.id"), + dirty: propertyNotEqual("originalTrustLevel", "trust_level"), saveTrustLevel() { return ajax(`/admin/users/${this.id}/trust_level`, { type: "PUT", - data: { level: this.get("trustLevel.id") } + data: { level: this.trust_level } }) .then(() => window.location.reload()) .catch(e => { @@ -266,7 +266,7 @@ const AdminUser = Discourse.User.extend({ }, restoreTrustLevel() { - this.set("trustLevel.id", this.originalTrustLevel); + this.set("trust_level", this.originalTrustLevel); }, lockTrustLevel(locked) { diff --git a/app/assets/javascripts/admin/templates/api-keys.hbs b/app/assets/javascripts/admin/templates/api-keys.hbs index d631e4188d..a1254f3042 100644 --- a/app/assets/javascripts/admin/templates/api-keys.hbs +++ b/app/assets/javascripts/admin/templates/api-keys.hbs @@ -3,6 +3,8 @@ {{i18n "admin.api.key"}} {{i18n "admin.api.user"}} + {{i18n "admin.api.created"}} + {{i18n "admin.api.last_used"}}   @@ -10,6 +12,7 @@ {{k.key}} +
{{i18n 'admin.api.user'}}
{{#if k.user}} {{#link-to "adminUser" k.user}} {{avatar k.user imageSize="small"}} @@ -18,6 +21,18 @@ {{i18n "admin.api.all_users"}} {{/if}} + +
{{i18n 'admin.api.created'}}
+ {{format-date k.created_at}} + + +
{{i18n 'admin.api.last_used'}}
+ {{#if k.last_used_at}} + {{format-date k.last_used_at}} + {{else}} + {{i18n "admin.api.never_used"}} + {{/if}} + {{d-button class="btn-default" diff --git a/app/assets/javascripts/admin/templates/components/admin-editable-field.hbs b/app/assets/javascripts/admin/templates/components/admin-editable-field.hbs index 354707edb5..ee44e3c9bc 100644 --- a/app/assets/javascripts/admin/templates/components/admin-editable-field.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-editable-field.hbs @@ -1,7 +1,7 @@
{{i18n name}}
{{#if editing}} - {{text-field value=buffer autofocus="autofocus"}} + {{text-field value=buffer autofocus="autofocus" autocomplete="discourse"}} {{else}} {{value}} {{/if}} diff --git a/app/assets/javascripts/admin/templates/customize-colors-show.hbs b/app/assets/javascripts/admin/templates/customize-colors-show.hbs index 521cd8688c..da53bb9808 100644 --- a/app/assets/javascripts/admin/templates/customize-colors-show.hbs +++ b/app/assets/javascripts/admin/templates/customize-colors-show.hbs @@ -40,9 +40,8 @@ {{#each colors as |c|}} - {{c.translatedName}} -
- {{c.description}} +

{{c.translatedName}}

+

{{c.description}}

{{color-input hexValue=c.hex brightnessValue=c.brightness valid=c.valid}} diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs index 8617ff29c5..6166eb1b9a 100644 --- a/app/assets/javascripts/admin/templates/user-index.hbs +++ b/app/assets/javascripts/admin/templates/user-index.hbs @@ -369,8 +369,9 @@
{{combo-box content=site.trustLevels - value=model.trustLevel.id - nameProperty="detailedName"}} + value=model.trust_level + nameProperty="detailedName" + }} {{#if model.dirty}}
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 180df64af2..033e043d43 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -19,7 +19,7 @@ //= require ./discourse/lib/hash //= require ./discourse/lib/load-script //= require ./discourse/lib/notification-levels -//= require ./discourse/lib/app-events +//= require ./discourse/services/app-events //= require ./discourse/lib/offset-calculator //= require ./discourse/lib/lock-on //= require ./discourse/lib/url diff --git a/app/assets/javascripts/discourse-common/mixins/focus-event.js.es6 b/app/assets/javascripts/discourse-common/mixins/focus-event.js.es6 index 7aafebdcce..c0395b53ac 100644 --- a/app/assets/javascripts/discourse-common/mixins/focus-event.js.es6 +++ b/app/assets/javascripts/discourse-common/mixins/focus-event.js.es6 @@ -1,40 +1,43 @@ -function gotFocus() { - if (!Discourse.get("hasFocus")) { - Discourse.setProperties({ hasFocus: true, notify: false }); - } -} - -function lostFocus() { - if (Discourse.get("hasFocus")) { - Discourse.set("hasFocus", false); - } -} - -let onchange; +import { getOwner } from "discourse-common/lib/get-owner"; export default Ember.Mixin.create({ ready() { this._super(...arguments); - onchange = () => { - document.visibilityState === "hidden" ? lostFocus() : gotFocus(); - }; + this._onChangeHandler = Ember.run.bind(this, this._onChange); // Default to true Discourse.set("hasFocus", true); - document.addEventListener("visibilitychange", onchange); - document.addEventListener("resume", onchange); - document.addEventListener("freeze", onchange); + document.addEventListener("visibilitychange", this._onChangeHandler); + document.addEventListener("resume", this._onChangeHandler); + document.addEventListener("freeze", this._onChangeHandler); }, reset() { this._super(...arguments); - document.removeEventListener("visibilitychange", onchange); - document.removeEventListener("resume", onchange); - document.removeEventListener("freeze", onchange); + document.removeEventListener("visibilitychange", this._onChangeHandler); + document.removeEventListener("resume", this._onChangeHandler); + document.removeEventListener("freeze", this._onChangeHandler); - onchange = undefined; + this._onChangeHandler = null; + }, + + _onChange() { + const container = getOwner(this); + const appEvents = container.lookup("app-events:main"); + + if (document.visibilityState === "hidden") { + if (Discourse.hasFocus) { + Discourse.set("hasFocus", false); + appEvents.trigger("discourse:focus-changed", false); + } + } else { + if (!Discourse.hasFocus) { + Discourse.set("hasFocus", true); + appEvents.trigger("discourse:focus-changed", true); + } + } } }); diff --git a/app/assets/javascripts/discourse-common/resolver.js.es6 b/app/assets/javascripts/discourse-common/resolver.js.es6 index 1b9dab0025..aeab2d45fa 100644 --- a/app/assets/javascripts/discourse-common/resolver.js.es6 +++ b/app/assets/javascripts/discourse-common/resolver.js.es6 @@ -1,4 +1,5 @@ import { findHelper } from "discourse-common/lib/helpers"; +import deprecated from "discourse-common/lib/deprecated"; /* global requirejs, require */ var classify = Ember.String.classify; @@ -45,6 +46,14 @@ export function buildResolver(baseName) { }, normalize(fullName) { + if (fullName === "app-events:main") { + deprecated( + "`app-events:main` has been replaced with `service:app-events`", + { since: "2.4.0" } + ); + return "service:app-events"; + } + const split = fullName.split(":"); if (split.length > 1) { const appBase = `${baseName}/${split[0]}s/`; diff --git a/app/assets/javascripts/discourse.js.es6 b/app/assets/javascripts/discourse.js.es6 index 3c072dfc86..d993e3b983 100644 --- a/app/assets/javascripts/discourse.js.es6 +++ b/app/assets/javascripts/discourse.js.es6 @@ -92,14 +92,6 @@ const Discourse = Ember.Application.extend(FocusEvent, { } }, - // The classes of buttons to show on a post - @computed - postButtons() { - return Discourse.SiteSettings.post_menu.split("|").map(function(i) { - return i.replace(/\+/, "").capitalize(); - }); - }, - updateContextCount(count) { this.set("contextCount", count); }, diff --git a/app/assets/javascripts/discourse/components/about-page-users.js.es6 b/app/assets/javascripts/discourse/components/about-page-users.js.es6 new file mode 100644 index 0000000000..e2d65135db --- /dev/null +++ b/app/assets/javascripts/discourse/components/about-page-users.js.es6 @@ -0,0 +1,24 @@ +import { userPath } from "discourse/lib/url"; +import { formatUsername, escapeExpression } from "discourse/lib/utilities"; +import { normalize } from "discourse/components/user-info"; +import { renderAvatar } from "discourse/helpers/user-avatar"; + +export default Ember.Component.extend({ + usersTemplates: Ember.computed("users.[]", function() { + return (this.users || []).map(user => { + let name = ""; + if (user.name && normalize(user.username) !== normalize(user.name)) { + name = user.name; + } + + return { + username: user.username, + name, + userPath: userPath(user.username), + avatar: renderAvatar(user, { imageSize: "large" }), + title: escapeExpression(user.title || ""), + formatedUsername: formatUsername(user.username) + }; + }); + }) +}); diff --git a/app/assets/javascripts/discourse/components/composer-body.js.es6 b/app/assets/javascripts/discourse/components/composer-body.js.es6 index b1af547ebb..301e22452f 100644 --- a/app/assets/javascripts/discourse/components/composer-body.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-body.js.es6 @@ -7,6 +7,7 @@ import afterTransition from "discourse/lib/after-transition"; import positioningWorkaround from "discourse/lib/safari-hacks"; import { headerHeight } from "discourse/components/site-header"; import KeyEnterEscape from "discourse/mixins/key-enter-escape"; +import { iOSWithVisualViewport } from "discourse/lib/utilities"; const START_EVENTS = "touchstart mousedown"; const DRAG_EVENTS = "touchmove mousemove"; @@ -132,7 +133,7 @@ export default Ember.Component.extend(KeyEnterEscape, { $document.on(END_EVENTS, endDrag); }); - if (window.visualViewport !== undefined) { + if (iOSWithVisualViewport()) { this.viewportResize(); window.visualViewport.addEventListener("resize", this.viewportResize); } @@ -141,11 +142,6 @@ export default Ember.Component.extend(KeyEnterEscape, { viewportResize() { const composerVH = window.visualViewport.height * 0.01; - if (window.visualViewport.height !== window.innerHeight) { - document.documentElement.classList.add("keyboard-visible"); - } else { - document.documentElement.classList.remove("keyboard-visible"); - } document.documentElement.style.setProperty( "--composer-vh", `${composerVH}px` @@ -174,7 +170,7 @@ export default Ember.Component.extend(KeyEnterEscape, { willDestroyElement() { this._super(...arguments); this.appEvents.off("composer:resize", this, this.resize); - if (window.visualViewport !== undefined) { + if (iOSWithVisualViewport()) { window.visualViewport.removeEventListener("resize", this.viewportResize); } }, diff --git a/app/assets/javascripts/discourse/components/d-editor-modal.js.es6 b/app/assets/javascripts/discourse/components/d-editor-modal.js.es6 deleted file mode 100644 index 4085b8cc8f..0000000000 --- a/app/assets/javascripts/discourse/components/d-editor-modal.js.es6 +++ /dev/null @@ -1,61 +0,0 @@ -import { observes, on } from "ember-addons/ember-computed-decorators"; - -export default Ember.Component.extend({ - classNameBindings: [":d-editor-modal", "hidden"], - - @observes("hidden") - _hiddenChanged() { - if (!this.hidden) { - Ember.run.scheduleOnce("afterRender", () => { - const $modal = $(this.element); - const $parent = $(this.element).closest(".d-editor"); - const w = $parent.width(); - const h = $parent.height(); - const dir = $("html").css("direction") === "rtl" ? "right" : "left"; - const offset = w / 2 - $modal.outerWidth() / 2; - $modal.css(dir, offset + "px"); - parent - .$(".d-editor-overlay") - .removeClass("hidden") - .css({ width: w, height: h }); - this.$("input:eq(0)").focus(); - }); - } else { - parent.$(".d-editor-overlay").addClass("hidden"); - } - }, - - @on("didInsertElement") - _listenKeys() { - $(this.element).on("keydown.d-modal", key => { - if (this.hidden) { - return; - } - - if (key.keyCode === 27) { - this.send("cancel"); - return false; - } - if (key.keyCode === 13) { - this.send("ok"); - return false; - } - }); - }, - - @on("willDestroyElement") - _stopListening() { - $(this.element).off("keydown.d-modal"); - }, - - actions: { - ok() { - this.set("hidden", true); - this.okAction(); - }, - - cancel() { - this.set("hidden", true); - } - } -}); diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index 8cd3c541ba..dc2e9fe00a 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -21,6 +21,7 @@ import { wantsNewWindow } from "discourse/lib/intercept-click"; import { translations } from "pretty-text/emoji/data"; import { emojiSearch, isSkinTonableEmoji } from "pretty-text/emoji"; import { emojiUrlFor } from "discourse/lib/text"; +import showModal from "discourse/lib/show-modal"; // Our head can be a static string or a function that returns a string // based on input (like for numbered lists). @@ -89,7 +90,7 @@ class Toolbar { id: "link", group: "insertions", shortcut: "K", - action: (...args) => this.context.send("showLinkModal", args) + sendAction: event => this.context.send("showLinkModal", event) }); } @@ -213,9 +214,6 @@ export function onToolbarCreate(func) { export default Ember.Component.extend({ classNames: ["d-editor"], ready: false, - insertLinkHidden: true, - linkUrl: "", - linkText: "", lastSel: null, _mouseTrap: null, showLink: true, @@ -946,21 +944,23 @@ export default Ember.Component.extend({ } }, - showLinkModal() { + showLinkModal(toolbarEvent) { if (this.disabled) { return; } - this.set("linkUrl", ""); - this.set("linkText", ""); - + let linkText = ""; this._lastSel = this._getSelected(); if (this._lastSel) { - this.set("linkText", this._lastSel.value.trim()); + linkText = this._lastSel.value.trim(); } - this.set("insertLinkHidden", false); + showModal("insert-hyperlink").setProperties({ + linkText: linkText, + _lastSel: this._lastSel, + toolbarEvent + }); }, formatCode() { @@ -1004,29 +1004,6 @@ export default Ember.Component.extend({ ); } } - }, - - insertLink() { - const origLink = this.linkUrl; - const linkUrl = - origLink.indexOf("://") === -1 ? `http://${origLink}` : origLink; - const sel = this._lastSel; - - if (Ember.isEmpty(linkUrl)) { - return; - } - - const linkText = this.linkText || ""; - if (linkText.length) { - this._addText(sel, `[${linkText}](${linkUrl})`); - } else { - if (sel.value) { - this._addText(sel, `[${sel.value}](${linkUrl})`); - } else { - this._addText(sel, `[${origLink}](${linkUrl})`); - this._selectText(sel.start + 1, origLink.length); - } - } } } }); diff --git a/app/assets/javascripts/discourse/components/d-modal.js.es6 b/app/assets/javascripts/discourse/components/d-modal.js.es6 index f870780789..8c46fc48e1 100644 --- a/app/assets/javascripts/discourse/components/d-modal.js.es6 +++ b/app/assets/javascripts/discourse/components/d-modal.js.es6 @@ -33,6 +33,10 @@ export default Ember.Component.extend({ if (e.which === 27 && this.dismissable) { Ember.run.next(() => $(".modal-header a.close").click()); } + + if (e.which === 13 && this.triggerClickOnEnter(e)) { + Ember.run.next(() => $(".modal-footer .btn-primary").click()); + } }); this.appEvents.on("modal:body-shown", this, "_modalBodyShown"); @@ -44,6 +48,18 @@ export default Ember.Component.extend({ this.appEvents.off("modal:body-shown", this, "_modalBodyShown"); }, + triggerClickOnEnter(e) { + // skip when in a form or a textarea element + if ( + e.target.closest("form") || + (document.activeElement && document.activeElement.nodeName === "TEXTAREA") + ) { + return false; + } + + return true; + }, + mouseDown(e) { if (!this.dismissable) { return; diff --git a/app/assets/javascripts/discourse/components/discourse-topic.js.es6 b/app/assets/javascripts/discourse/components/discourse-topic.js.es6 index f94788b470..7e3011fb6e 100644 --- a/app/assets/javascripts/discourse/components/discourse-topic.js.es6 +++ b/app/assets/javascripts/discourse/components/discourse-topic.js.es6 @@ -110,6 +110,7 @@ export default Ember.Component.extend( } ); + this.appEvents.on("discourse:focus-changed", this, "gotFocus"); this.appEvents.on("post:highlight", this, "_highlightPost"); this.appEvents.on("header:update-topic", this, "_updateTopic"); }, @@ -129,13 +130,13 @@ export default Ember.Component.extend( // this happens after route exit, stuff could have trickled in this._hideTopicInHeader(); + this.appEvents.off("discourse:focus-changed", this, "gotFocus"); this.appEvents.off("post:highlight", this, "_highlightPost"); this.appEvents.off("header:update-topic", this, "_updateTopic"); }, - @observes("Discourse.hasFocus") - gotFocus() { - if (Discourse.get("hasFocus")) { + gotFocus(hasFocus) { + if (hasFocus) { this.scrolled(); } }, diff --git a/app/assets/javascripts/discourse/components/plugin-connector.js.es6 b/app/assets/javascripts/discourse/components/plugin-connector.js.es6 index b5df63aa8f..76590147ce 100644 --- a/app/assets/javascripts/discourse/components/plugin-connector.js.es6 +++ b/app/assets/javascripts/discourse/components/plugin-connector.js.es6 @@ -16,6 +16,13 @@ export default Ember.Component.extend({ this.set("actions", connectorClass.actions); }, + willDestroyElement() { + this._super(...arguments); + + const connectorClass = this.get("connector.connectorClass"); + connectorClass.teardownComponent.call(this, this); + }, + @observes("args") _argsChanged() { const args = this.args || {}; diff --git a/app/assets/javascripts/discourse/components/second-factor-form.js.es6 b/app/assets/javascripts/discourse/components/second-factor-form.js.es6 index f990437ce5..572fd509f7 100644 --- a/app/assets/javascripts/discourse/components/second-factor-form.js.es6 +++ b/app/assets/javascripts/discourse/components/second-factor-form.js.es6 @@ -4,16 +4,26 @@ import { SECOND_FACTOR_METHODS } from "discourse/models/user"; export default Ember.Component.extend({ @computed("secondFactorMethod") secondFactorTitle(secondFactorMethod) { - return secondFactorMethod === SECOND_FACTOR_METHODS.TOTP - ? I18n.t("login.second_factor_title") - : I18n.t("login.second_factor_backup_title"); + switch (secondFactorMethod) { + case SECOND_FACTOR_METHODS.TOTP: + return I18n.t("login.second_factor_title"); + case SECOND_FACTOR_METHODS.SECURITY_KEY: + return I18n.t("login.second_factor_title"); + case SECOND_FACTOR_METHODS.BACKUP_CODE: + return I18n.t("login.second_factor_backup_title"); + } }, @computed("secondFactorMethod") secondFactorDescription(secondFactorMethod) { - return secondFactorMethod === SECOND_FACTOR_METHODS.TOTP - ? I18n.t("login.second_factor_description") - : I18n.t("login.second_factor_backup_description"); + switch (secondFactorMethod) { + case SECOND_FACTOR_METHODS.TOTP: + return I18n.t("login.second_factor_description"); + case SECOND_FACTOR_METHODS.SECURITY_KEY: + return I18n.t("login.security_key_description"); + case SECOND_FACTOR_METHODS.BACKUP_CODE: + return I18n.t("login.second_factor_backup_description"); + } }, @computed("secondFactorMethod", "isLogin") @@ -29,6 +39,13 @@ export default Ember.Component.extend({ } }, + @computed("backupEnabled", "secondFactorMethod") + showToggleMethodLink(backupEnabled, secondFactorMethod) { + return ( + backupEnabled && secondFactorMethod !== SECOND_FACTOR_METHODS.SECURITY_KEY + ); + }, + actions: { toggleSecondFactorMethod() { const secondFactorMethod = this.secondFactorMethod; diff --git a/app/assets/javascripts/discourse/components/security-key-form.js.es6 b/app/assets/javascripts/discourse/components/security-key-form.js.es6 new file mode 100644 index 0000000000..3161831869 --- /dev/null +++ b/app/assets/javascripts/discourse/components/security-key-form.js.es6 @@ -0,0 +1,11 @@ +import { SECOND_FACTOR_METHODS } from "discourse/models/user"; + +export default Ember.Component.extend({ + actions: { + useAnotherMethod() { + this.set("showSecurityKey", false); + this.set("showSecondFactor", true); + this.set("secondFactorMethod", SECOND_FACTOR_METHODS.TOTP); + } + } +}); diff --git a/app/assets/javascripts/discourse/components/topic-progress.js.es6 b/app/assets/javascripts/discourse/components/topic-progress.js.es6 index 41c57d073c..e37d5821c2 100644 --- a/app/assets/javascripts/discourse/components/topic-progress.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-progress.js.es6 @@ -161,7 +161,7 @@ export default Ember.Component.extend({ : $("#topic-progress").outerHeight(); const maximumOffset = $("#topic-bottom").offset().top + progressHeight; const windowHeight = $(window).height(); - const composerHeight = $("#reply-control").height() || 0; + let composerHeight = $("#reply-control").height() || 0; const isDocked = offset >= maximumOffset - windowHeight + composerHeight; let bottom = $("body").height() - maximumOffset; @@ -170,8 +170,15 @@ export default Ember.Component.extend({ bottom += $iPadFooterNav.outerHeight(); } const wrapperDir = $html.hasClass("rtl") ? "left" : "right"; + const draftComposerHeight = 40; if (composerHeight > 0) { + const $iPhoneFooterNav = $(".footer-nav-visible .footer-nav"); + const $replyDraft = $("#reply-control.draft"); + if ($iPhoneFooterNav.outerHeight() && $replyDraft.outerHeight()) { + composerHeight = + $replyDraft.outerHeight() + $iPhoneFooterNav.outerHeight(); + } $wrapper.css("bottom", isDocked ? bottom : composerHeight); } else { $wrapper.css("bottom", isDocked ? bottom : ""); @@ -185,6 +192,11 @@ export default Ember.Component.extend({ } else { $wrapper.css(wrapperDir, "1em"); } + + $wrapper.css( + "margin-bottom", + !isDocked && composerHeight > draftComposerHeight ? "0px" : "" + ); }, click(e) { diff --git a/app/assets/javascripts/discourse/components/topic-timeline.js.es6 b/app/assets/javascripts/discourse/components/topic-timeline.js.es6 index 12e9aefea6..17709a5fc7 100644 --- a/app/assets/javascripts/discourse/components/topic-timeline.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-timeline.js.es6 @@ -3,7 +3,14 @@ import Docking from "discourse/mixins/docking"; import { observes } from "ember-addons/ember-computed-decorators"; import optionalService from "discourse/lib/optional-service"; -const headerPadding = () => parseInt($("#main-outlet").css("padding-top")) + 3; +const headerPadding = () => { + let topPadding = parseInt($("#main-outlet").css("padding-top")) + 3; + const iPadNavHeight = $(".footer-nav-ipad .footer-nav").height(); + if (iPadNavHeight) { + topPadding += iPadNavHeight; + } + return topPadding; +}; export default MountWidget.extend(Docking, { adminTools: optionalService(), diff --git a/app/assets/javascripts/discourse/components/user-info.js.es6 b/app/assets/javascripts/discourse/components/user-info.js.es6 index e507b30570..21a1347775 100644 --- a/app/assets/javascripts/discourse/components/user-info.js.es6 +++ b/app/assets/javascripts/discourse/components/user-info.js.es6 @@ -1,7 +1,7 @@ import computed from "ember-addons/ember-computed-decorators"; import { userPath } from "discourse/lib/url"; -function normalize(name) { +export function normalize(name) { return name.replace(/[\-\_ \.]/g, "").toLowerCase(); } diff --git a/app/assets/javascripts/discourse/controllers/email-login.js.es6 b/app/assets/javascripts/discourse/controllers/email-login.js.es6 index 5a025ce7a6..d062144afe 100644 --- a/app/assets/javascripts/discourse/controllers/email-login.js.es6 +++ b/app/assets/javascripts/discourse/controllers/email-login.js.es6 @@ -1,20 +1,40 @@ +import computed from "ember-addons/ember-computed-decorators"; import { SECOND_FACTOR_METHODS } from "discourse/models/user"; import { ajax } from "discourse/lib/ajax"; import DiscourseURL from "discourse/lib/url"; import { popupAjaxError } from "discourse/lib/ajax-error"; +import { getWebauthnCredential } from "discourse/lib/webauthn"; export default Ember.Controller.extend({ - secondFactorMethod: SECOND_FACTOR_METHODS.TOTP, lockImageUrl: Discourse.getURL("/images/lock.svg"), + + @computed("model") + secondFactorRequired(model) { + return model.security_key_required || model.second_factor_required; + }, + + @computed("model") + secondFactorMethod(model) { + return model.security_key_required + ? SECOND_FACTOR_METHODS.SECURITY_KEY + : SECOND_FACTOR_METHODS.TOTP; + }, + actions: { finishLogin() { + let data = {}; + if (this.securityKeyCredential) { + data = { security_key_credential: this.securityKeyCredential }; + } else { + data = { + second_factor_token: this.secondFactorToken, + second_factor_method: this.secondFactorMethod + }; + } ajax({ url: `/session/email-login/${this.model.token}`, type: "POST", - data: { - second_factor_token: this.secondFactorToken, - second_factor_method: this.secondFactorMethod - } + data: data }) .then(result => { if (result.success) { @@ -24,6 +44,19 @@ export default Ember.Controller.extend({ } }) .catch(popupAjaxError); + }, + authenticateSecurityKey() { + getWebauthnCredential( + this.model.challenge, + this.model.allowed_credential_ids, + credentialData => { + this.set("securityKeyCredential", credentialData); + this.send("finishLogin"); + }, + errorMessage => { + this.set("model.error", errorMessage); + } + ); } } }); diff --git a/app/assets/javascripts/discourse/controllers/insert-hyperlink.js.es6 b/app/assets/javascripts/discourse/controllers/insert-hyperlink.js.es6 new file mode 100644 index 0000000000..d4f15d95cf --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/insert-hyperlink.js.es6 @@ -0,0 +1,46 @@ +import ModalFunctionality from "discourse/mixins/modal-functionality"; + +export default Ember.Controller.extend(ModalFunctionality, { + linkUrl: "", + linkText: "", + + onShow() { + Ember.run.next(() => + $(this) + .find("input.link-url") + .focus() + ); + }, + + actions: { + ok() { + const origLink = this.linkUrl; + const linkUrl = + origLink.indexOf("://") === -1 ? `http://${origLink}` : origLink; + const sel = this._lastSel; + + if (Ember.isEmpty(linkUrl)) { + return; + } + + const linkText = this.linkText || ""; + + if (linkText.length) { + this.toolbarEvent.addText(`[${linkText}](${linkUrl})`); + } else { + if (sel.value) { + this.toolbarEvent.addText(`[${sel.value}](${linkUrl})`); + } else { + this.toolbarEvent.addText(`[${origLink}](${linkUrl})`); + this.toolbarEvent.selectText(sel.start + 1, origLink.length); + } + } + this.set("linkUrl", ""); + this.set("linkText", ""); + this.send("closeModal"); + }, + cancel() { + this.send("closeModal"); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6 index dfe7eaf68c..7f0f5bdd33 100644 --- a/app/assets/javascripts/discourse/controllers/login.js.es6 +++ b/app/assets/javascripts/discourse/controllers/login.js.es6 @@ -8,6 +8,7 @@ import { escapeExpression, areCookiesEnabled } from "discourse/lib/utilities"; import { extractError } from "discourse/lib/ajax-error"; import computed from "ember-addons/ember-computed-decorators"; import { SECOND_FACTOR_METHODS } from "discourse/models/user"; +import { getWebauthnCredential } from "discourse/lib/webauthn"; // This is happening outside of the app via popup const AuthErrors = [ @@ -23,7 +24,6 @@ export default Ember.Controller.extend(ModalFunctionality, { forgotPassword: Ember.inject.controller(), application: Ember.inject.controller(), - authenticate: null, loggingIn: false, loggedIn: false, processingEmailLink: false, @@ -38,24 +38,24 @@ export default Ember.Controller.extend(ModalFunctionality, { resetForm() { this.setProperties({ - authenticate: null, loggingIn: false, loggedIn: false, secondFactorRequired: false, showSecondFactor: false, + showSecurityKey: false, showLoginButtons: true, awaitingApproval: false }); }, - @computed("showSecondFactor") - credentialsClass(showSecondFactor) { - return showSecondFactor ? "hidden" : ""; + @computed("showSecondFactor", "showSecurityKey") + credentialsClass(showSecondFactor, showSecurityKey) { + return showSecondFactor || showSecurityKey ? "hidden" : ""; }, - @computed("showSecondFactor") - secondFactorClass(showSecondFactor) { - return showSecondFactor ? "" : "hidden"; + @computed("showSecondFactor", "showSecurityKey") + secondFactorClass(showSecondFactor, showSecurityKey) { + return showSecondFactor || showSecurityKey ? "" : "hidden"; }, @computed("awaitingApproval", "hasAtLeastOneLoginButton") @@ -66,6 +66,11 @@ export default Ember.Controller.extend(ModalFunctionality, { return classes.join(" "); }, + @computed("showSecondFactor", "showSecurityKey") + disableLoginFields(showSecondFactor, showSecurityKey) { + return showSecondFactor || showSecurityKey; + }, + @computed("canLoginLocalWithEmail") hasAtLeastOneLoginButton(canLoginLocalWithEmail) { return findAll().length > 0 || canLoginLocalWithEmail; @@ -78,12 +83,12 @@ export default Ember.Controller.extend(ModalFunctionality, { loginDisabled: Ember.computed.or("loggingIn", "loggedIn"), - @computed("loggingIn", "authenticate", "application.canSignUp") - showSignupLink(loggingIn, authenticate, canSignUp) { - return canSignUp && !loggingIn && Ember.isEmpty(authenticate); + @computed("loggingIn", "application.canSignUp") + showSignupLink(loggingIn, canSignUp) { + return canSignUp && !loggingIn; }, - showSpinner: Ember.computed.or("loggingIn", "authenticate"), + showSpinner: Ember.computed.readOnly("loggingIn"), @computed("canLoginLocalWithEmail", "processingEmailLink") showLoginWithEmailLink(canLoginLocalWithEmail, processingEmailLink) { @@ -109,15 +114,20 @@ export default Ember.Controller.extend(ModalFunctionality, { login: this.loginName, password: this.loginPassword, second_factor_token: this.secondFactorToken, - second_factor_method: this.secondFactorMethod + second_factor_method: this.secondFactorMethod, + security_key_credential: this.securityKeyCredential } }).then( result => { // Successful login if (result && result.error) { this.set("loggingIn", false); + const invalidSecurityKey = result.reason === "invalid_security_key"; + const invalidSecondFactor = + result.reason === "invalid_second_factor"; + if ( - result.reason === "invalid_second_factor" && + (invalidSecondFactor || invalidSecurityKey) && !this.secondFactorRequired ) { document.getElementById("modal-alert").style.display = "none"; @@ -126,15 +136,24 @@ export default Ember.Controller.extend(ModalFunctionality, { secondFactorRequired: true, showLoginButtons: false, backupEnabled: result.backup_enabled, - showSecondFactor: true + showSecondFactor: invalidSecondFactor, + showSecurityKey: invalidSecurityKey, + secondFactorMethod: invalidSecurityKey + ? SECOND_FACTOR_METHODS.SECURITY_KEY + : SECOND_FACTOR_METHODS.TOTP, + securityKeyChallenge: result.challenge, + securityKeyAllowedCredentialIds: result.allowed_credential_ids }); - Ember.run.schedule("afterRender", () => - document - .getElementById("second-factor") - .querySelector("input") - .focus() - ); + // only need to focus the 2FA input for TOTP + if (!this.showSecurityKey) { + Ember.run.scheduleOnce("afterRender", () => + document + .getElementById("second-factor") + .querySelector("input") + .focus() + ); + } return; } else if (result.reason === "not_activated") { @@ -212,20 +231,13 @@ export default Ember.Controller.extend(ModalFunctionality, { return false; }, - externalLogin(loginMethod, { fullScreenLogin = false } = {}) { - const capabilities = this.capabilities; - // On Mobile, Android or iOS always go with full screen - if ( - this.isMobileDevice || - (capabilities && - (capabilities.isIOS || - capabilities.isAndroid || - capabilities.isSafari)) - ) { - fullScreenLogin = true; + externalLogin(loginMethod) { + if (this.loginDisabled) { + return; } - loginMethod.doLogin({ fullScreenLogin }); + this.set("loggingIn", true); + loginMethod.doLogin().catch(() => this.set("loggingIn", false)); }, createAccount() { @@ -286,16 +298,20 @@ export default Ember.Controller.extend(ModalFunctionality, { }) .catch(e => this.flash(extractError(e), "error")) .finally(() => this.set("processingEmailLink", false)); - } - }, + }, - @computed("authenticate") - authMessage(authenticate) { - if (Ember.isEmpty(authenticate)) return ""; - - const method = findAll().findBy("name", authenticate); - if (method) { - return method.message; + authenticateSecurityKey() { + getWebauthnCredential( + this.securityKeyChallenge, + this.securityKeyAllowedCredentialIds, + credentialData => { + this.set("securityKeyCredential", credentialData); + this.send("login"); + }, + errorMessage => { + this.flash(errorMessage, "error"); + } + ); } }, @@ -306,7 +322,6 @@ export default Ember.Controller.extend(ModalFunctionality, { Ember.run.next(() => { if (callback) callback(); this.flash(errorMsg, className || "success"); - this.set("authenticate", null); }); }; diff --git a/app/assets/javascripts/discourse/controllers/password-reset.js.es6 b/app/assets/javascripts/discourse/controllers/password-reset.js.es6 index 2d48d74e2e..ae3c5949a5 100644 --- a/app/assets/javascripts/discourse/controllers/password-reset.js.es6 +++ b/app/assets/javascripts/discourse/controllers/password-reset.js.es6 @@ -4,13 +4,21 @@ import { ajax } from "discourse/lib/ajax"; import PasswordValidation from "discourse/mixins/password-validation"; import { userPath } from "discourse/lib/url"; import { SECOND_FACTOR_METHODS } from "discourse/models/user"; +import { getWebauthnCredential } from "discourse/lib/webauthn"; export default Ember.Controller.extend(PasswordValidation, { isDeveloper: Ember.computed.alias("model.is_developer"), admin: Ember.computed.alias("model.admin"), secondFactorRequired: Ember.computed.alias("model.second_factor_required"), + securityKeyRequired: Ember.computed.alias("model.security_key_required"), backupEnabled: Ember.computed.alias("model.backup_enabled"), - secondFactorMethod: SECOND_FACTOR_METHODS.TOTP, + securityKeyOrSecondFactorRequired: Ember.computed.or( + "model.second_factor_required", + "model.security_key_required" + ), + secondFactorMethod: Ember.computed.alias("model.security_key_required") + ? SECOND_FACTOR_METHODS.SECURITY_KEY + : SECOND_FACTOR_METHODS.TOTP, passwordRequired: true, errorMessage: null, successMessage: null, @@ -39,7 +47,8 @@ export default Ember.Controller.extend(PasswordValidation, { data: { password: this.accountPassword, second_factor_token: this.secondFactorToken, - second_factor_method: this.secondFactorMethod + second_factor_method: this.secondFactorMethod, + security_key_credential: this.securityKeyCredential } }) .then(result => { @@ -53,15 +62,17 @@ export default Ember.Controller.extend(PasswordValidation, { DiscourseURL.redirectTo(result.redirect_to || "/"); } } else { - if (result.errors && result.errors.user_second_factors) { + if (result.errors && !result.errors.password) { this.setProperties({ - secondFactorRequired: true, + secondFactorRequired: this.secondFactorRequired, + securityKeyRequired: this.securityKeyRequired, password: null, errorMessage: result.message }); - } else if (this.secondFactorRequired) { + } else if (this.secondFactorRequired || this.securityKeyRequired) { this.setProperties({ secondFactorRequired: false, + securityKeyRequired: false, errorMessage: null }); } else if ( @@ -90,6 +101,24 @@ export default Ember.Controller.extend(PasswordValidation, { }); }, + authenticateSecurityKey() { + getWebauthnCredential( + this.model.challenge, + this.model.allowed_credential_ids, + credentialData => { + this.set("securityKeyCredential", credentialData); + this.send("submit"); + }, + errorMessage => { + this.setProperties({ + securityKeyRequired: true, + password: null, + errorMessage: errorMessage + }); + } + ); + }, + done() { this.set("redirected", true); DiscourseURL.redirectTo(this.redirectTo || "/"); diff --git a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 index 8bf5d24d27..14fcc4b79f 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 @@ -252,7 +252,7 @@ export default Ember.Controller.extend( }, connectAccount(method) { - method.doLogin({ reconnect: true, fullScreenLogin: false }); + method.doLogin({ reconnect: true }); } } } diff --git a/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 index 072c42c3b6..e5b7bf42bf 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 @@ -10,7 +10,11 @@ import { setLocalTheme } from "discourse/lib/theme-selector"; import { popupAjaxError } from "discourse/lib/ajax-error"; -import { safariHacksDisabled, isiPad } from "discourse/lib/utilities"; +import { + safariHacksDisabled, + isiPad, + iOSWithVisualViewport +} from "discourse/lib/utilities"; const USER_HOMES = { 1: "latest", @@ -51,7 +55,9 @@ export default Ember.Controller.extend(PreferencesTabController, { @computed() isiPad() { - return isiPad(); + // TODO: remove this preference checkbox when iOS adoption > 90% + // (currently only applies to iOS 12 and below) + return isiPad() && !iOSWithVisualViewport(); }, @computed() diff --git a/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 index 85129d453e..34eb99d0e1 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 @@ -68,12 +68,14 @@ export default Ember.Controller.extend(CanCheckEmails, { errorMessage: null, loaded: true, totps: response.totps, + security_keys: response.security_keys, password: null, dirty: false }); this.set( "model.second_factor_enabled", - response.totps && response.totps.length > 0 + (response.totps && response.totps.length > 0) || + (response.security_keys && response.security_keys.length > 0) ); }) .catch(e => this.handleError(e)) @@ -147,6 +149,31 @@ export default Ember.Controller.extend(CanCheckEmails, { }); }, + createSecurityKey() { + const controller = showModal("second-factor-add-security-key", { + model: this.model, + title: "user.second_factor.security_key.add" + }); + controller.setProperties({ + onClose: () => this.loadSecondFactors(), + markDirty: () => this.markDirty(), + onError: e => this.handleError(e) + }); + }, + + editSecurityKey(security_key) { + const controller = showModal("second-factor-edit-security-key", { + model: security_key, + title: "user.second_factor.security_key.edit" + }); + controller.setProperties({ + user: this.model, + onClose: () => this.loadSecondFactors(), + markDirty: () => this.markDirty(), + onError: e => this.handleError(e) + }); + }, + editSecondFactor(second_factor) { const controller = showModal("second-factor-edit", { model: second_factor, diff --git a/app/assets/javascripts/discourse/controllers/second-factor-add-security-key.js.es6 b/app/assets/javascripts/discourse/controllers/second-factor-add-security-key.js.es6 new file mode 100644 index 0000000000..f19bb5a0f7 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/second-factor-add-security-key.js.es6 @@ -0,0 +1,136 @@ +import ModalFunctionality from "discourse/mixins/modal-functionality"; +import { + bufferToBase64, + stringToBuffer, + isWebauthnSupported +} from "discourse/lib/webauthn"; + +// model for this controller is user.js.es6 +export default Ember.Controller.extend(ModalFunctionality, { + loading: false, + errorMessage: null, + + onShow() { + // clear properties every time because the controller is a singleton + this.setProperties({ + errorMessage: null, + loading: true, + securityKeyName: I18n.t("user.second_factor.security_key.default_name"), + webauthnUnsupported: !isWebauthnSupported() + }); + + this.model + .requestSecurityKeyChallenge() + .then(response => { + if (response.error) { + this.set("errorMessage", response.error); + return; + } + + this.setProperties({ + errorMessage: isWebauthnSupported() + ? null + : I18n.t("login.security_key_support_missing_error"), + loading: false, + challenge: response.challenge, + relayingParty: { + id: response.rp_id, + name: response.rp_name + }, + supported_algoriths: response.supported_algoriths, + user_secure_id: response.user_secure_id, + existing_active_credential_ids: + response.existing_active_credential_ids + }); + }) + .catch(error => { + this.send("closeModal"); + this.onError(error); + }) + .finally(() => this.set("loading", false)); + }, + + actions: { + registerSecurityKey() { + const publicKeyCredentialCreationOptions = { + challenge: Uint8Array.from(this.challenge, c => c.charCodeAt(0)), + rp: { + name: this.relayingParty.name, + id: this.relayingParty.id + }, + user: { + id: Uint8Array.from(this.user_secure_id, c => c.charCodeAt(0)), + displayName: this.model.username_lower, + name: this.model.username_lower + }, + pubKeyCredParams: this.supported_algoriths.map(alg => { + return { type: "public-key", alg: alg }; + }), + excludeCredentials: this.existing_active_credential_ids.map( + credentialId => { + return { + type: "public-key", + id: stringToBuffer(atob(credentialId)) + }; + } + ), + timeout: 20000, + attestation: "none", + authenticatorSelection: { + // see https://chromium.googlesource.com/chromium/src/+/master/content/browser/webauth/uv_preferred.md for why + // default value of preferred is not necesarrily what we want, it limits webauthn to only devices that support + // user verification, which usually requires entering a PIN + userVerification: "discouraged" + } + }; + + navigator.credentials + .create({ + publicKey: publicKeyCredentialCreationOptions + }) + .then( + credential => { + let serverData = { + id: credential.id, + rawId: bufferToBase64(credential.rawId), + type: credential.type, + attestation: bufferToBase64( + credential.response.attestationObject + ), + clientData: bufferToBase64(credential.response.clientDataJSON), + name: this.securityKeyName + }; + + this.model + .registerSecurityKey(serverData) + .then(response => { + if (response.error) { + this.set("errorMessage", response.error); + return; + } + this.markDirty(); + this.set("errorMessage", null); + this.send("closeModal"); + }) + .catch(error => this.onError(error)) + .finally(() => this.set("loading", false)); + }, + err => { + if (err.name === "InvalidStateError") { + return this.set( + "errorMessage", + I18n.t("user.second_factor.security_key.already_added_error") + ); + } + if (err.name === "NotAllowedError") { + return this.set( + "errorMessage", + I18n.t("user.second_factor.security_key.not_allowed_error") + ); + } + this.set("errorMessage", err.message); + } + ); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/second-factor-add-totp.js.es6 b/app/assets/javascripts/discourse/controllers/second-factor-add-totp.js.es6 index e826f934cb..ac105f57b2 100644 --- a/app/assets/javascripts/discourse/controllers/second-factor-add-totp.js.es6 +++ b/app/assets/javascripts/discourse/controllers/second-factor-add-totp.js.es6 @@ -11,6 +11,7 @@ export default Ember.Controller.extend(ModalFunctionality, { this.setProperties({ errorMessage: null, secondFactorKey: null, + secondFactorName: null, secondFactorToken: null, showSecondFactorKey: false, secondFactorImage: null, @@ -47,10 +48,7 @@ export default Ember.Controller.extend(ModalFunctionality, { this.set("loading", true); this.model - .enableSecondFactorTotp( - this.secondFactorToken, - I18n.t("user.second_factor.totp.default_name") - ) + .enableSecondFactorTotp(this.secondFactorToken, this.secondFactorName) .then(response => { if (response.error) { this.set("errorMessage", response.error); diff --git a/app/assets/javascripts/discourse/controllers/second-factor-edit-security-key.js.es6 b/app/assets/javascripts/discourse/controllers/second-factor-edit-security-key.js.es6 new file mode 100644 index 0000000000..90815cfbea --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/second-factor-edit-security-key.js.es6 @@ -0,0 +1,42 @@ +import ModalFunctionality from "discourse/mixins/modal-functionality"; + +export default Ember.Controller.extend(ModalFunctionality, { + actions: { + disableSecurityKey() { + this.user + .updateSecurityKey(this.model.id, this.model.name, true) + .then(response => { + if (response.error) { + return; + } + this.markDirty(); + }) + .catch(error => { + this.send("closeModal"); + this.onError(error); + }) + .finally(() => { + this.set("loading", false); + this.send("closeModal"); + }); + }, + + editSecurityKey() { + this.user + .updateSecurityKey(this.model.id, this.model.name, false) + .then(response => { + if (response.error) { + return; + } + this.markDirty(); + }) + .catch(error => { + this.onError(error); + }) + .finally(() => { + this.set("loading", false); + this.send("closeModal"); + }); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 6cb4ecf0be..f459e4bcec 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -471,6 +471,8 @@ export default Ember.Controller.extend(bufferedProperty("model"), { const quoteState = this.quoteState; const postStream = this.get("model.postStream"); + this.appEvents.trigger("page:compose-reply", topic); + if (!postStream || !topic || !topic.get("details.can_create_post")) { return; } @@ -705,6 +707,12 @@ export default Ember.Controller.extend(bufferedProperty("model"), { }); }, + jumpEnd() { + DiscourseURL.routeTo(this.get("model.lastPostUrl"), { + jumpEnd: true + }); + }, + jumpUnread() { this._jumpToPostId(this.get("model.last_read_post_id")); }, @@ -937,46 +945,6 @@ export default Ember.Controller.extend(bufferedProperty("model"), { } }, - joinGroup() { - const groupId = this.get("model.group.id"); - if (groupId) { - if (this.get("model.group.allow_membership_requests")) { - const groupName = this.get("model.group.name"); - return ajax(`/groups/${groupName}/request_membership`, { - type: "POST", - data: { - topic_id: this.get("model.id") - } - }) - .then(() => { - bootbox.alert( - I18n.t("topic.group_request_sent", { - group_name: this.get("model.group.full_name") - }), - () => - this.previousURL - ? DiscourseURL.routeTo(this.previousURL) - : DiscourseURL.routeTo("/") - ); - }) - .catch(popupAjaxError); - } else { - const topic = this.model; - return ajax(`/groups/${groupId}/members`, { - type: "PUT", - data: { user_id: this.get("currentUser.id") } - }) - .then(() => - topic.reload().then(() => { - topic.set("view_hidden", false); - topic.postStream.refresh(); - }) - ) - .catch(popupAjaxError); - } - } - }, - replyAsNewTopic(post, quotedText) { const composerController = this.composer; @@ -1174,7 +1142,7 @@ export default Ember.Controller.extend(bufferedProperty("model"), { } }, - hasError: Ember.computed.or("model.notFoundHtml", "model.message"), + hasError: Ember.computed.or("model.errorHtml", "model.errorMessage"), noErrorYet: Ember.computed.not("hasError"), categories: Ember.computed.alias("site.categoriesList"), diff --git a/app/assets/javascripts/discourse/initializers/auth-complete.js.es6 b/app/assets/javascripts/discourse/initializers/auth-complete.js.es6 index cecf831405..83e0183ff3 100644 --- a/app/assets/javascripts/discourse/initializers/auth-complete.js.es6 +++ b/app/assets/javascripts/discourse/initializers/auth-complete.js.es6 @@ -4,11 +4,7 @@ export default { initialize(container) { let lastAuthResult; - if (window.location.search.indexOf("authComplete=true") !== -1) { - // Happens when a popup social login loses connection to the parent window - lastAuthResult = localStorage.getItem("lastAuthResult"); - localStorage.removeItem("lastAuthResult"); - } else if (document.getElementById("data-authentication")) { + if (document.getElementById("data-authentication")) { // Happens for full screen logins lastAuthResult = document.getElementById("data-authentication").dataset .authenticationData; diff --git a/app/assets/javascripts/discourse/initializers/avatar-select.js.es6 b/app/assets/javascripts/discourse/initializers/avatar-select.js.es6 index f35ee67540..9eaac83db9 100644 --- a/app/assets/javascripts/discourse/initializers/avatar-select.js.es6 +++ b/app/assets/javascripts/discourse/initializers/avatar-select.js.es6 @@ -10,7 +10,7 @@ export default { ).selectable_avatars_enabled; container - .lookup("app-events:main") + .lookup("service:app-events") .on("show-avatar-select", this, "_showAvatarSelect"); }, diff --git a/app/assets/javascripts/discourse/initializers/badging.js.es6 b/app/assets/javascripts/discourse/initializers/badging.js.es6 index b80416c231..004764056e 100644 --- a/app/assets/javascripts/discourse/initializers/badging.js.es6 +++ b/app/assets/javascripts/discourse/initializers/badging.js.es6 @@ -13,7 +13,7 @@ export default { user.unread_notifications + user.unread_private_messages; container - .lookup("app-events:main") + .lookup("service:app-events") .on("notifications:changed", this, "_updateBadge"); }, diff --git a/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 b/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 index f11760ec6c..71fa6f25ae 100644 --- a/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 +++ b/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 @@ -17,7 +17,7 @@ export default { router.on("routeWillChange", viewTrackingRequired); router.on("routeDidChange", cleanDOM); - let appEvents = container.lookup("app-events:main"); + let appEvents = container.lookup("service:app-events"); startPageTracking(router, appEvents); diff --git a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 index b71f37aaca..d3c251bb88 100644 --- a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 @@ -18,7 +18,7 @@ export default { initialize(container) { const user = container.lookup("current-user:main"); const bus = container.lookup("message-bus:main"); - const appEvents = container.lookup("app-events:main"); + const appEvents = container.lookup("service:app-events"); if (user) { bus.subscribe("/reviewable_counts", data => { diff --git a/app/assets/javascripts/discourse/initializers/title-notifications.js.es6 b/app/assets/javascripts/discourse/initializers/title-notifications.js.es6 index ca83d7a639..9801e1f688 100644 --- a/app/assets/javascripts/discourse/initializers/title-notifications.js.es6 +++ b/app/assets/javascripts/discourse/initializers/title-notifications.js.es6 @@ -9,7 +9,7 @@ export default { this.container = container; container - .lookup("app-events:main") + .lookup("service:app-events") .on("notifications:changed", this, "_updateTitle"); }, diff --git a/app/assets/javascripts/discourse/lib/clean-dom.js.es6 b/app/assets/javascripts/discourse/lib/clean-dom.js.es6 index 59157189f6..8d83f4d7f8 100644 --- a/app/assets/javascripts/discourse/lib/clean-dom.js.es6 +++ b/app/assets/javascripts/discourse/lib/clean-dom.js.es6 @@ -30,7 +30,7 @@ function _clean() { } // TODO: Avoid container lookup here - const appEvents = Discourse.__container__.lookup("app-events:main"); + const appEvents = Discourse.__container__.lookup("service:app-events"); appEvents.trigger("dom:clean"); } diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 index 664a1cbdc6..e051f479ff 100644 --- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 +++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 @@ -86,7 +86,7 @@ export default { this._stopCallback(); this.searchService = this.container.lookup("search-service:main"); - this.appEvents = this.container.lookup("app-events:main"); + this.appEvents = this.container.lookup("service:app-events"); this.currentUser = this.container.lookup("current-user:main"); let siteSettings = this.container.lookup("site-settings:main"); diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 index 950fdcd166..4cf670b3de 100644 --- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 @@ -449,7 +449,7 @@ class PluginApi { ``` **/ onAppEvent(name, fn) { - const appEvents = this._lookupContainer("app-events:main"); + const appEvents = this._lookupContainer("service:app-events"); appEvents && appEvents.on(name, fn); } diff --git a/app/assets/javascripts/discourse/lib/plugin-connectors.js.es6 b/app/assets/javascripts/discourse/lib/plugin-connectors.js.es6 index 3b817746a4..b58d19825a 100644 --- a/app/assets/javascripts/discourse/lib/plugin-connectors.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-connectors.js.es6 @@ -17,7 +17,8 @@ export function extraConnectorClass(name, obj) { const DefaultConnectorClass = { actions: {}, shouldRender: () => true, - setupComponent() {} + setupComponent() {}, + teardownComponent() {} }; function findOutlets(collection, callback) { diff --git a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 index 644fa2c436..1fb5318045 100644 --- a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 +++ b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 @@ -1,5 +1,8 @@ import debounce from "discourse/lib/debounce"; -import { safariHacksDisabled } from "discourse/lib/utilities"; +import { + safariHacksDisabled, + iOSWithVisualViewport +} from "discourse/lib/utilities"; // TODO: remove calcHeight once iOS 13 adoption > 90% // In iOS 13 and up we use visualViewport API to calculate height @@ -75,27 +78,21 @@ export function isWorkaroundActive() { function positioningWorkaround($fixedElement) { const caps = Discourse.__container__.lookup("capabilities:main"); - if (!caps.isIOS || caps.isIpadOS || safariHacksDisabled()) { + if (!caps.isIOS || safariHacksDisabled()) { return; } const fixedElement = $fixedElement[0]; const oldHeight = fixedElement.style.height; - var done = false; var originalScrollTop = 0; + let lastTouchedElement = null; positioningWorkaround.blur = function(evt) { if (workaroundActive) { - done = true; + $("body").removeClass("ios-safari-composer-hacks"); - $("#main-outlet").show(); - $("header").show(); - - fixedElement.style.position = ""; - fixedElement.style.top = ""; - - if (window.visualViewport === undefined) { + if (!iOSWithVisualViewport()) { fixedElement.style.height = oldHeight; Ember.run.later( () => $(fixedElement).removeClass("no-transition"), @@ -113,14 +110,17 @@ function positioningWorkaround($fixedElement) { }; var blurredNow = function(evt) { + // we cannot use evt.relatedTarget to get the last focused element in safari iOS + // document.activeElement is also unreliable (iOS does not mark buttons as focused) + // so instead, we store the last touched element and check against it + if ( - !done && - $(document.activeElement) - .parents() - .toArray() - .indexOf(fixedElement) > -1 + lastTouchedElement && + ($(lastTouchedElement).hasClass("select-kit-header") || + ["span", "svg", "button"].includes( + lastTouchedElement.nodeName.toLowerCase() + )) ) { - // something in focus so skip return; } @@ -130,60 +130,79 @@ function positioningWorkaround($fixedElement) { var blurred = debounce(blurredNow, 250); var positioningHack = function(evt) { - done = false; - // we need this, otherwise changing focus means we never clear this.addEventListener("blur", blurred); - if (fixedElement.style.top === "0px") { - if (this !== document.activeElement) { - evt.preventDefault(); + // resets focus out of select-kit elements + // might become redundant after select-kit refactoring + $fixedElement.find(".select-kit.is-expanded > button").trigger("click"); + $fixedElement + .find(".select-kit > button.is-focused") + .removeClass("is-focused"); - // this tricks safari into assuming current input is at top of the viewport - // via https://stackoverflow.com/questions/38017771/mobile-safari-prevent-scroll-page-when-focus-on-input - this.style.transform = "translateY(-200px)"; - this.focus(); - let _this = this; - setTimeout(function() { - _this.style.transform = "none"; - }, 50); + if ($(window).scrollTop() > 0) { + originalScrollTop = $(window).scrollTop(); + } + + setTimeout(function() { + if (iOSWithVisualViewport()) { + // disable hacks when using a hardware keyboard + // by default, a hardware keyboard will show the keyboard accessory bar + // whose height is currently 55px (using 75 for a bit of a buffer) + let heightDiff = window.innerHeight - window.visualViewport.height; + if (heightDiff < 75) { + return; + } } - return; - } - // don't trigger keyboard on disabled element (happens when a category is required) - if (this.disabled) { - return; - } + if (fixedElement.style.top === "0px") { + if (this !== document.activeElement) { + evt.preventDefault(); - originalScrollTop = $(window).scrollTop(); + // this tricks safari into assuming current input is at top of the viewport + // via https://stackoverflow.com/questions/38017771/mobile-safari-prevent-scroll-page-when-focus-on-input + this.style.transform = "translateY(-200px)"; + this.focus(); + let _this = this; + setTimeout(function() { + _this.style.transform = "none"; + }, 30); + } + return; + } - // take care of body + // don't trigger keyboard on disabled element (happens when a category is required) + if (this.disabled) { + return; + } - $("#main-outlet").hide(); - $("header").hide(); - - $(window).scrollTop(0); - - let i = 20; - let interval = setInterval(() => { + $("body").addClass("ios-safari-composer-hacks"); $(window).scrollTop(0); - if (i-- === 0) { - clearInterval(interval); + + let i = 20; + let interval = setInterval(() => { + $(window).scrollTop(0); + if (i-- === 0) { + clearInterval(interval); + } + }, 10); + + if (!iOSWithVisualViewport()) { + const height = calcHeight(); + fixedElement.style.height = height + "px"; + $(fixedElement).addClass("no-transition"); } - }, 10); - fixedElement.style.top = "0px"; + evt.preventDefault(); + this.focus(); + workaroundActive = true; + }, 350); + }; - if (window.visualViewport === undefined) { - const height = calcHeight(); - fixedElement.style.height = height + "px"; - $(fixedElement).addClass("no-transition"); + var lastTouched = function(evt) { + if (evt && evt.target) { + lastTouchedElement = evt.target; } - - evt.preventDefault(); - this.focus(); - workaroundActive = true; }; function attachTouchStart(elem, fn) { @@ -194,30 +213,8 @@ function positioningWorkaround($fixedElement) { } const checkForInputs = debounce(function() { - $fixedElement - .find( - "button:not(.hide-preview),a:not(.mobile-file-upload):not(.toggle-toolbar)" - ) - .each(function(idx, elem) { - if ($(elem).parents(".emoji-picker").length > 0) { - return; - } + attachTouchStart(fixedElement, lastTouched); - if ($(elem).parents(".autocomplete").length > 0) { - return; - } - - if ($(elem).parents(".d-editor-button-bar").length > 0) { - return; - } - - attachTouchStart(this, function(evt) { - done = true; - $(document.activeElement).blur(); - evt.preventDefault(); - $(this).click(); - }); - }); $fixedElement.find("input[type=text],textarea").each(function() { attachTouchStart(this, positioningHack); }); diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6 index 66b82829ec..34bb600e07 100644 --- a/app/assets/javascripts/discourse/lib/url.js.es6 +++ b/app/assets/javascripts/discourse/lib/url.js.es6 @@ -93,6 +93,12 @@ const DiscourseURL = Ember.Object.extend({ let elementId; let holder; + if (opts.jumpEnd) { + $(window).scrollTop($(document).height() - $(window).height()); + _transitioning = false; + return; + } + if (postNumber === 1 && !opts.anchor) { $(window).scrollTop(0); _transitioning = false; @@ -347,7 +353,8 @@ const DiscourseURL = Ember.Object.extend({ this.appEvents.trigger("post:highlight", closest); const jumpOpts = { - skipIfOnScreen: routeOpts.skipIfOnScreen + skipIfOnScreen: routeOpts.skipIfOnScreen, + jumpEnd: routeOpts.jumpEnd }; const m = /#.+$/.exec(path); @@ -398,6 +405,9 @@ const DiscourseURL = Ember.Object.extend({ ); }, + // TODO: These container calls can be replaced eventually if we migrate this to a service + // object. + /** @private @@ -410,6 +420,10 @@ const DiscourseURL = Ember.Object.extend({ return Discourse.__container__.lookup("router:main"); }, + get appEvents() { + return Discourse.__container__.lookup("service:app-events"); + }, + // Get a controller. Note that currently it uses `__container__` which is not // advised but there is no other way to access the router. controllerFor(name) { diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6 index 530f21d6ac..599c3a4965 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js.es6 +++ b/app/assets/javascripts/discourse/lib/utilities.js.es6 @@ -544,6 +544,10 @@ export function isAppleDevice() { let iPadDetected = undefined; +export function iOSWithVisualViewport() { + return isAppleDevice() && window.visualViewport !== undefined; +} + export function isiPad() { if (iPadDetected === undefined) { iPadDetected = @@ -554,6 +558,8 @@ export function isiPad() { } export function safariHacksDisabled() { + if (iOSWithVisualViewport()) return false; + let pref = localStorage.getItem("safari-hacks-disabled"); let result = false; if (pref !== null) { diff --git a/app/assets/javascripts/discourse/lib/webauthn.js.es6 b/app/assets/javascripts/discourse/lib/webauthn.js.es6 new file mode 100644 index 0000000000..6b3e81ad4d --- /dev/null +++ b/app/assets/javascripts/discourse/lib/webauthn.js.es6 @@ -0,0 +1,78 @@ +export function stringToBuffer(str) { + let buffer = new ArrayBuffer(str.length); + let byteView = new Uint8Array(buffer); + for (let i = 0; i < str.length; i++) { + byteView[i] = str.charCodeAt(i); + } + return buffer; +} + +export function bufferToBase64(buffer) { + return btoa(String.fromCharCode(...new Uint8Array(buffer))); +} + +export function isWebauthnSupported() { + return typeof PublicKeyCredential !== "undefined"; +} + +export function getWebauthnCredential( + challenge, + allowedCredentialIds, + successCallback, + errorCallback +) { + if (!isWebauthnSupported()) { + return errorCallback(I18n.t("login.security_key_support_missing_error")); + } + + let challengeBuffer = stringToBuffer(challenge); + let allowCredentials = allowedCredentialIds.map(credentialId => { + return { + id: stringToBuffer(atob(credentialId)), + type: "public-key" + }; + }); + + navigator.credentials + .get({ + publicKey: { + challenge: challengeBuffer, + allowCredentials: allowCredentials, + timeout: 60000, + + // see https://chromium.googlesource.com/chromium/src/+/master/content/browser/webauth/uv_preferred.md for why + // default value of preferred is not necesarrily what we want, it limits webauthn to only devices that support + // user verification, which usually requires entering a PIN + userVerification: "discouraged" + } + }) + .then(credential => { + // 1. if there is a credential, check if the raw ID base64 matches + // any of the allowed credential ids + if ( + !allowedCredentialIds.some( + credentialId => bufferToBase64(credential.rawId) === credentialId + ) + ) { + return errorCallback( + I18n.t("login.security_key_no_matching_credential_error") + ); + } + + const credentialData = { + signature: bufferToBase64(credential.response.signature), + clientData: bufferToBase64(credential.response.clientDataJSON), + authenticatorData: bufferToBase64( + credential.response.authenticatorData + ), + credentialId: bufferToBase64(credential.rawId) + }; + successCallback(credentialData); + }) + .catch(err => { + if (err.name === "NotAllowedError") { + return errorCallback(I18n.t("login.security_key_not_allowed_error")); + } + errorCallback(err); + }); +} diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index ebe6d5aa6e..de9d9042bc 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -941,6 +941,11 @@ const Composer = RestModel.extend({ composer.clearState(); composer.set("createdPost", createdPost); + if (composer.replyingToTopic) { + this.appEvents.trigger("post:created", createdPost); + } else { + this.appEvents.trigger("topic:created", createdPost, composer); + } if (addedToStream) { composer.set("composeState", CLOSED); diff --git a/app/assets/javascripts/discourse/models/login-method.js.es6 b/app/assets/javascripts/discourse/models/login-method.js.es6 index 0b1260447a..ce59db2450 100644 --- a/app/assets/javascripts/discourse/models/login-method.js.es6 +++ b/app/assets/javascripts/discourse/models/login-method.js.es6 @@ -17,60 +17,24 @@ const LoginMethod = Ember.Object.extend({ return this.message_override || I18n.t(`login.${this.name}.message`); }, - doLogin({ reconnect = false, fullScreenLogin = true } = {}) { - const name = this.name; - const customLogin = this.customLogin; - - if (customLogin) { - customLogin(); - } else { - if (this.custom_url) { - window.location = this.custom_url; - return; - } - let authUrl = Discourse.getURL(`/auth/${name}`); - - if (reconnect) { - authUrl += "?reconnect=true"; - } - - if (reconnect || fullScreenLogin || this.full_screen_login) { - LoginMethod.buildPostForm(authUrl).then(form => { - document.cookie = "fsl=true"; - form.submit(); - }); - } else { - this.set("authenticate", name); - const left = this.lastX - 400; - const top = this.lastY - 200; - - const height = this.frame_height || 400; - const width = this.frame_width || 800; - - if (name === "facebook") { - authUrl += authUrl.includes("?") ? "&" : "?"; - authUrl += "display=popup"; - } - LoginMethod.buildPostForm(authUrl).then(form => { - const windowState = window.open( - "about:blank", - "auth_popup", - `menubar=no,status=no,height=${height},width=${width},left=${left},top=${top}` - ); - - form.target = "auth_popup"; - form.submit(); - - const timer = setInterval(() => { - // If the process is aborted, reset state in this window - if (!windowState || windowState.closed) { - clearInterval(timer); - this.set("authenticate", null); - } - }, 1000); - }); - } + doLogin({ reconnect = false } = {}) { + if (this.customLogin) { + this.customLogin(); + return Ember.RSVP.resolve(); } + + if (this.custom_url) { + window.location = this.custom_url; + return Ember.RSVP.resolve(); + } + + let authUrl = Discourse.getURL(`/auth/${this.name}`); + + if (reconnect) { + authUrl += "?reconnect=true"; + } + + return LoginMethod.buildPostForm(authUrl).then(form => form.submit()); } }); diff --git a/app/assets/javascripts/discourse/models/post-stream.js.es6 b/app/assets/javascripts/discourse/models/post-stream.js.es6 index 346226b9eb..a3b42bcaeb 100644 --- a/app/assets/javascripts/discourse/models/post-stream.js.es6 +++ b/app/assets/javascripts/discourse/models/post-stream.js.es6 @@ -1067,31 +1067,16 @@ export default RestModel.extend({ // Handles an error loading a topic based on a HTTP status code. Updates // the text to the correct values. errorLoading(result) { - const status = result.jqXHR.status; - const topic = this.topic; this.set("loadingFilter", false); topic.set("errorLoading", true); - // If the result was 404 the post is not found - // If it was 410 the post is deleted and the user should not see it - if (status === 404 || status === 410) { - topic.set("notFoundHtml", result.jqXHR.responseText); - return; + const json = result.jqXHR.responseJSON; + if (json && json.extras && json.extras.html) { + topic.set("errorHtml", json.extras.html); + } else { + topic.set("errorMessage", I18n.t("topic.server_error.description")); + topic.set("noRetry", result.jqXHR.status === 403); } - - // If the result is 403 it means invalid access - if (status === 403) { - topic.set("noRetry", true); - if (Discourse.User.current()) { - topic.set("message", I18n.t("topic.invalid_access.description")); - } else { - topic.set("message", I18n.t("topic.invalid_access.login_required")); - } - return; - } - - // Otherwise supply a generic error message - topic.set("message", I18n.t("topic.server_error.description")); } }); diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6 index 9a9fbe8e63..a82cef491d 100644 --- a/app/assets/javascripts/discourse/models/post.js.es6 +++ b/app/assets/javascripts/discourse/models/post.js.es6 @@ -316,7 +316,10 @@ const Post = RestModel.extend({ // need to wait to hear back from server (stuff may not be loaded) return Discourse.Post.updateBookmark(this.id, this.bookmarked) - .then(result => this.set("topic.bookmarked", result.topic_bookmarked)) + .then(result => { + this.set("topic.bookmarked", result.topic_bookmarked); + this.appEvents.trigger("page:bookmark-post-toggled", this); + }) .catch(error => { this.toggleProperty("bookmarked"); if (bookmarkedTopic) { diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index d44afe930e..1af3697106 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -17,16 +17,15 @@ import { } from "ember-addons/ember-computed-decorators"; export function loadTopicView(topic, args) { - const topicId = topic.get("id"); const data = _.merge({}, args); - const url = `${Discourse.getURL("/t/")}${topicId}`; + const url = `${Discourse.getURL("/t/")}${topic.id}`; const jsonUrl = (data.nearPost ? `${url}/${data.nearPost}` : url) + ".json"; delete data.nearPost; delete data.__type; delete data.store; - return PreloadStore.getAndRemove(`topic_${topicId}`, () => + return PreloadStore.getAndRemove(`topic_${topic.id}`, () => ajax(jsonUrl, { data }) ).then(json => { topic.updateFromJson(json); diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 84ac5efed4..a16b24f80e 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -21,7 +21,11 @@ import { defaultHomepage } from "discourse/lib/utilities"; import { userPath } from "discourse/lib/url"; import Category from "discourse/models/category"; -export const SECOND_FACTOR_METHODS = { TOTP: 1, BACKUP_CODE: 2 }; +export const SECOND_FACTOR_METHODS = { + TOTP: 1, + BACKUP_CODE: 2, + SECURITY_KEY: 3 +}; const isForever = dt => moment().diff(dt, "years") < -500; @@ -375,6 +379,19 @@ const User = RestModel.extend({ }); }, + requestSecurityKeyChallenge() { + return ajax("/u/create_second_factor_security_key.json", { + type: "POST" + }); + }, + + registerSecurityKey(credential) { + return ajax("/u/register_second_factor_security_key.json", { + data: credential, + type: "POST" + }); + }, + createSecondFactorTotp() { return ajax("/u/create_second_factor_totp.json", { type: "POST" @@ -409,6 +426,17 @@ const User = RestModel.extend({ }); }, + updateSecurityKey(id, name, disable) { + return ajax("/u/security_key.json", { + data: { + name, + disable, + id + }, + type: "PUT" + }); + }, + toggleSecondFactor(authToken, authMethod, targetMethod, enable) { return ajax("/u/second_factor.json", { data: { diff --git a/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 b/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 index 7e1d8142e7..2555cf6d12 100644 --- a/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 +++ b/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 @@ -1,8 +1,6 @@ import Session from "discourse/models/session"; import KeyValueStore from "discourse/lib/key-value-store"; -import AppEvents from "discourse/lib/app-events"; import Store from "discourse/models/store"; -import DiscourseURL from "discourse/lib/url"; import DiscourseLocation from "discourse/lib/discourse-location"; import SearchService from "discourse/services/search"; import { @@ -17,10 +15,7 @@ export default { name: "inject-discourse-objects", initialize(container, app) { - const appEvents = AppEvents.create(); - app.register("app-events:main", appEvents, { instantiate: false }); - ALL_TARGETS.forEach(t => app.inject(t, "appEvents", "app-events:main")); - DiscourseURL.appEvents = appEvents; + ALL_TARGETS.forEach(t => app.inject(t, "appEvents", "service:app-events")); // backwards compatibility: remove when plugins have updated app.register("store:main", Store); diff --git a/app/assets/javascripts/discourse/routes/application.js.es6 b/app/assets/javascripts/discourse/routes/application.js.es6 index f9cdcab186..4c85a42862 100644 --- a/app/assets/javascripts/discourse/routes/application.js.es6 +++ b/app/assets/javascripts/discourse/routes/application.js.es6 @@ -265,9 +265,7 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, { const methods = findAll(); if (!this.siteSettings.enable_local_logins && methods.length === 1) { - this.controllerFor("login").send("externalLogin", methods[0], { - fullScreenLogin: true - }); + this.controllerFor("login").send("externalLogin", methods[0]); } else { showModal(modal); this.controllerFor("modal").set("modalClass", modalClass); diff --git a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 index b2a5a00ae5..aadf72eb35 100644 --- a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 @@ -198,8 +198,24 @@ export default (filterArg, params) => { }, actions: { + error(err) { + const json = err.jqXHR.responseJSON; + if (json && json.extras && json.extras.html) { + this.controllerFor("discovery").set( + "errorHtml", + err.jqXHR.responseJSON.extras.html + ); + } else { + this.replaceWith("exception"); + } + }, + setNotification(notification_level) { this.currentModel.setNotification(notification_level); + }, + + triggerRefresh() { + this.refresh(); } } }); diff --git a/app/assets/javascripts/discourse/routes/preferences-second-factor.js.es6 b/app/assets/javascripts/discourse/routes/preferences-second-factor.js.es6 index f3a7c2ea7b..763460bc23 100644 --- a/app/assets/javascripts/discourse/routes/preferences-second-factor.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences-second-factor.js.es6 @@ -14,6 +14,7 @@ export default RestrictedUserRoute.extend({ setupController(controller, model) { controller.setProperties({ model, newUsername: model.get("username") }); controller.set("loading", true); + model .loadSecondFactorCodes("") .then(response => { @@ -24,7 +25,8 @@ export default RestrictedUserRoute.extend({ errorMessage: null, loaded: !response.password_required, dirty: !!response.password_required, - totps: response.totps + totps: response.totps, + security_keys: response.security_keys }); } }) diff --git a/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 b/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 index 70f56403e9..c193cbe98c 100644 --- a/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 @@ -52,6 +52,7 @@ export default Discourse.Route.extend({ enteredAt: new Date().getTime().toString() }); + this.appEvents.trigger("page:topic-loaded", topic); topicController.subscribe(); // Highlight our post after the next render diff --git a/app/assets/javascripts/discourse/lib/app-events.js.es6 b/app/assets/javascripts/discourse/services/app-events.js.es6 similarity index 95% rename from app/assets/javascripts/discourse/lib/app-events.js.es6 rename to app/assets/javascripts/discourse/services/app-events.js.es6 index aff71a2a40..5967568ef3 100644 --- a/app/assets/javascripts/discourse/lib/app-events.js.es6 +++ b/app/assets/javascripts/discourse/services/app-events.js.es6 @@ -1,6 +1,6 @@ import deprecated from "discourse-common/lib/deprecated"; -export default Ember.Object.extend(Ember.Evented, { +export default Ember.Service.extend(Ember.Evented, { _events: {}, on() { diff --git a/app/assets/javascripts/discourse/templates/about.hbs b/app/assets/javascripts/discourse/templates/about.hbs index e9be344032..b09252ff5e 100644 --- a/app/assets/javascripts/discourse/templates/about.hbs +++ b/app/assets/javascripts/discourse/templates/about.hbs @@ -28,9 +28,7 @@

{{d-icon "users"}} {{i18n 'about.our_admins'}}

- {{#each model.admins as |a|}} - {{user-info user=a}} - {{/each}} + {{about-page-users users=model.admins}}
@@ -45,9 +43,7 @@

{{d-icon "users"}} {{i18n 'about.our_moderators'}}

- {{#each model.moderators as |m|}} - {{user-info user=m}} - {{/each}} + {{about-page-users users=model.moderators}}
@@ -62,9 +58,7 @@

{{category-link cm.category}}{{i18n "about.moderators"}}

- {{#each cm.moderators as |m|}} - {{user-info user=m}} - {{/each}} + {{about-page-users users=cm.moderators}}
diff --git a/app/assets/javascripts/discourse/templates/components/about-page-users.hbs b/app/assets/javascripts/discourse/templates/components/about-page-users.hbs new file mode 100644 index 0000000000..4c946af466 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/about-page-users.hbs @@ -0,0 +1,22 @@ +{{#each usersTemplates as |userTemplate|}} + +{{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/cancel-link.hbs b/app/assets/javascripts/discourse/templates/components/cancel-link.hbs index 8c3a3cb4c7..4455e8b8d4 100644 --- a/app/assets/javascripts/discourse/templates/components/cancel-link.hbs +++ b/app/assets/javascripts/discourse/templates/components/cancel-link.hbs @@ -1,3 +1,3 @@ -{{#link-to route args}} +{{#link-to route args class="cancel"}} {{i18n 'cancel'}} {{/link-to}} diff --git a/app/assets/javascripts/discourse/templates/components/d-editor-modal.hbs b/app/assets/javascripts/discourse/templates/components/d-editor-modal.hbs deleted file mode 100644 index 9c1426e41e..0000000000 --- a/app/assets/javascripts/discourse/templates/components/d-editor-modal.hbs +++ /dev/null @@ -1,8 +0,0 @@ -{{#unless hidden}} - {{yield}} - -
- {{d-button class="btn-primary" label="composer.modal_ok" action=(action "ok")}} - {{d-button class="btn-danger" label="composer.modal_cancel" action=(action "cancel")}} -
-{{/unless}} diff --git a/app/assets/javascripts/discourse/templates/components/d-editor.hbs b/app/assets/javascripts/discourse/templates/components/d-editor.hbs index 1f82566145..9435d744f8 100644 --- a/app/assets/javascripts/discourse/templates/components/d-editor.hbs +++ b/app/assets/javascripts/discourse/templates/components/d-editor.hbs @@ -1,13 +1,3 @@ - - -
- {{#d-editor-modal class="insert-link" hidden=insertLinkHidden okAction=(action "insertLink")}} -

{{i18n "composer.link_dialog_title"}}

- {{text-field value=linkUrl placeholderKey="composer.link_url_placeholder" class="link-url"}} - {{text-field value=linkText placeholderKey="composer.link_optional_text" class="link-text"}} - {{/d-editor-modal}} -
-
diff --git a/app/assets/javascripts/discourse/templates/components/ip-lookup.hbs b/app/assets/javascripts/discourse/templates/components/ip-lookup.hbs index ebcd9220e5..291f5b54ee 100644 --- a/app/assets/javascripts/discourse/templates/components/ip-lookup.hbs +++ b/app/assets/javascripts/discourse/templates/components/ip-lookup.hbs @@ -8,14 +8,9 @@
{{d-icon "times"}} {{#if copied}} - - {{d-icon "copy"}} - {{i18n "ip_lookup.copied"}} - + {{d-button class="btn-hover pull-right" icon="copy" label="ip_lookup.copied"}} {{else}} - - {{d-icon "copy"}} - + {{d-button action=(action "copy") class="pull-right no-text" icon="copy"}} {{/if}}

{{i18n "ip_lookup.title"}}

{{{i18n "ip_lookup.powered_by"}}}

diff --git a/app/assets/javascripts/discourse/templates/components/second-factor-form.hbs b/app/assets/javascripts/discourse/templates/components/second-factor-form.hbs index f2ccf505df..4c5c717be3 100644 --- a/app/assets/javascripts/discourse/templates/components/second-factor-form.hbs +++ b/app/assets/javascripts/discourse/templates/components/second-factor-form.hbs @@ -5,12 +5,9 @@ {{/if}}

{{secondFactorDescription}}

{{yield}} - {{#if backupEnabled}} + {{#if showToggleMethodLink}}

- {{discourse-linked-text - class="toggle-second-factor-method" - action=(action "toggleSecondFactorMethod") - text=linkText}} + {{ i18n linkText }}

{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/second-factor-input.hbs b/app/assets/javascripts/discourse/templates/components/second-factor-input.hbs index a17e0deb7b..8a0d24d145 100644 --- a/app/assets/javascripts/discourse/templates/components/second-factor-input.hbs +++ b/app/assets/javascripts/discourse/templates/components/second-factor-input.hbs @@ -6,4 +6,5 @@ id=inputId autocorrect="off" autocapitalize="off" - autofocus="autofocus"}} + autofocus="autofocus" + placeholder=placeholder}} diff --git a/app/assets/javascripts/discourse/templates/components/security-key-form.hbs b/app/assets/javascripts/discourse/templates/components/security-key-form.hbs new file mode 100644 index 0000000000..e5bc247ad3 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/security-key-form.hbs @@ -0,0 +1,14 @@ +
+ {{d-button + action=action + icon="key" + id="security-key-authenticate-button" + label="login.security_key_authenticate" + type="button" + class='btn btn-large btn-primary'}} +

+ {{#if otherMethodAllowed}} + {{ i18n 'login.security_key_alternative' }} + {{/if}} +

+
diff --git a/app/assets/javascripts/discourse/templates/discovery.hbs b/app/assets/javascripts/discourse/templates/discovery.hbs index c6242e9e5b..6801ab3f55 100644 --- a/app/assets/javascripts/discourse/templates/discovery.hbs +++ b/app/assets/javascripts/discourse/templates/discovery.hbs @@ -1,32 +1,36 @@ -
- {{discourse-banner user=currentUser banner=site.banner}} -
- -
+{{#if errorHtml}} + {{{errorHtml}}} +{{else}}
- {{outlet "navigation-bar"}} + {{discourse-banner user=currentUser banner=site.banner}}
-
-{{conditional-loading-spinner condition=loading}} +
+
+ {{outlet "navigation-bar"}} +
+
-
-
-
-
- {{outlet "header-list-container"}} + {{conditional-loading-spinner condition=loading}} + +
+
+
+
+ {{outlet "header-list-container"}} +
+
+
+
+
+
+ {{plugin-outlet name="discovery-list-container-top" + args=(hash category=category listLoading=loading)}} + {{outlet "list-container"}} +
-
-
-
- {{plugin-outlet name="discovery-list-container-top" - args=(hash category=category listLoading=loading)}} - {{outlet "list-container"}} -
-
-
-
-{{plugin-outlet name="discovery-below"}} + {{plugin-outlet name="discovery-below"}} +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/email-login.hbs b/app/assets/javascripts/discourse/templates/email-login.hbs index 1556a212a2..4541fa8689 100644 --- a/app/assets/javascripts/discourse/templates/email-login.hbs +++ b/app/assets/javascripts/discourse/templates/email-login.hbs @@ -12,20 +12,34 @@ {{/if}} {{#if model.can_login}} - {{#if model.second_factor_required}} - {{#second-factor-form - secondFactorMethod=secondFactorMethod - secondFactorToken=secondFactorToken - backupEnabled=model.backup_codes_enabled - isLogin=true}} - {{second-factor-input value=secondFactorToken secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}} - {{/second-factor-form}} + {{#if secondFactorRequired }} + {{#if model.security_key_required }} + {{#security-key-form + allowedCredentialIds=model.allowed_credential_ids + challenge=model.security_key_challenge + showSecurityKey=model.security_key_required + showSecondFactor=false + secondFactorMethod=secondFactorMethod + otherMethodAllowed=secondFactorRequired + action=(action "authenticateSecurityKey")}} + {{/security-key-form}} + {{else}} + {{#second-factor-form + secondFactorMethod=secondFactorMethod + secondFactorToken=secondFactorToken + backupEnabled=model.backup_codes_enabled + isLogin=true}} + {{second-factor-input value=secondFactorToken secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}} + {{/second-factor-form}} + {{/if}} {{else}}

{{i18n "email_login.confirm_title" site_name=siteSettings.title}}

{{i18n "email_login.logging_in_as" email=model.token_email}}

{{/if}} - {{d-button label="email_login.confirm_button" action=(action "finishLogin") class="btn-primary"}} + {{#unless model.security_key_required }} + {{d-button label="email_login.confirm_button" action=(action "finishLogin") class="btn-primary"}} + {{/unless}} {{/if}}
diff --git a/app/assets/javascripts/discourse/templates/mobile/modal/login.hbs b/app/assets/javascripts/discourse/templates/mobile/modal/login.hbs index 594ea48603..b2d65fbe87 100644 --- a/app/assets/javascripts/discourse/templates/mobile/modal/login.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/modal/login.hbs @@ -40,8 +40,21 @@ secondFactorMethod=secondFactorMethod secondFactorToken=secondFactorToken class=secondFactorClass + backupEnabled=backupEnabled isLogin=true}} - {{second-factor-input value=secondFactorToken inputId='login-second-factor' secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}} + {{#if showSecurityKey}} + {{#security-key-form + allowedCredentialIds=securityKeyAllowedCredentialIds + challenge=securityKeyChallenge + showSecurityKey=showSecurityKey + showSecondFactor=showSecondFactor + secondFactorMethod=secondFactorMethod + otherMethodAllowed=secondFactorRequired + action=(action "authenticateSecurityKey")}} + {{/security-key-form}} + {{else}} + {{second-factor-input value=secondFactorToken inputId='login-second-factor' secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}} + {{/if}} {{/second-factor-form}} {{/if}} @@ -49,24 +62,22 @@ {{/d-modal-body}} -
{{authMessage}}
+
{{alert}}
{{/login-modal}} diff --git a/app/assets/javascripts/discourse/templates/modal/forgot-password.hbs b/app/assets/javascripts/discourse/templates/modal/forgot-password.hbs index 81e02f94bc..bd558cb03d 100644 --- a/app/assets/javascripts/discourse/templates/modal/forgot-password.hbs +++ b/app/assets/javascripts/discourse/templates/modal/forgot-password.hbs @@ -20,6 +20,7 @@ {{#unless helpSeen}} {{d-button class="btn-large" label="forgot_password.button_help" + icon="question-circle" action=(action "help")}} {{/unless}} {{/unless}} diff --git a/app/assets/javascripts/discourse/templates/modal/insert-hyperlink.hbs b/app/assets/javascripts/discourse/templates/modal/insert-hyperlink.hbs new file mode 100644 index 0000000000..8aa0607266 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/modal/insert-hyperlink.hbs @@ -0,0 +1,13 @@ +{{#d-modal-body title="composer.link_dialog_title" class="insert-link"}} +
+ {{text-field value=linkUrl placeholderKey="composer.link_url_placeholder" class="link-url"}} +
+
+ {{text-field value=linkText placeholderKey="composer.link_optional_text" class="link-text"}} +
+{{/d-modal-body}} + + diff --git a/app/assets/javascripts/discourse/templates/modal/login.hbs b/app/assets/javascripts/discourse/templates/modal/login.hbs index 326fdd4999..beaa5569ca 100644 --- a/app/assets/javascripts/discourse/templates/modal/login.hbs +++ b/app/assets/javascripts/discourse/templates/modal/login.hbs @@ -8,11 +8,11 @@ - + - + @@ -28,7 +28,19 @@ class=secondFactorClass backupEnabled=backupEnabled isLogin=true}} - {{second-factor-input value=secondFactorToken inputId='login-second-factor' secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}} + {{#if showSecurityKey}} + {{#security-key-form + allowedCredentialIds=securityKeyAllowedCredentialIds + challenge=securityKeyChallenge + showSecurityKey=showSecurityKey + showSecondFactor=showSecondFactor + secondFactorMethod=secondFactorMethod + otherMethodAllowed=secondFactorRequired + action=(action "authenticateSecurityKey")}} + {{/security-key-form}} + {{else}} + {{second-factor-input value=secondFactorToken inputId='login-second-factor' secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}} + {{/if}} {{/second-factor-form}} {{/if}} @@ -43,13 +55,16 @@ -
{{authMessage}}
{{alert}}
{{/login-modal}} diff --git a/app/assets/javascripts/discourse/templates/modal/second-factor-add-security-key.hbs b/app/assets/javascripts/discourse/templates/modal/second-factor-add-security-key.hbs new file mode 100644 index 0000000000..a035eea0ea --- /dev/null +++ b/app/assets/javascripts/discourse/templates/modal/second-factor-add-security-key.hbs @@ -0,0 +1,33 @@ +{{#d-modal-body}} + {{#conditional-loading-spinner condition=loading}} + {{#if errorMessage}} +
+
+
{{errorMessage}}
+
+
+ {{/if}} + +
+
+ {{{i18n 'user.second_factor.enable_security_key_description'}}} +
+
+ +
+
+ {{input value=securityKeyName id='test' placeholder='security key name'}} +
+
+ +
+
+ {{#unless webauthnUnsupported}} + {{d-button action=(action "registerSecurityKey") + class="btn btn-primary add-totp" + label="user.second_factor.security_key.register"}} + {{/unless}} +
+
+ {{/conditional-loading-spinner}} +{{/d-modal-body}} diff --git a/app/assets/javascripts/discourse/templates/modal/second-factor-add-totp.hbs b/app/assets/javascripts/discourse/templates/modal/second-factor-add-totp.hbs index 4f44cd0678..80a13f8f30 100644 --- a/app/assets/javascripts/discourse/templates/modal/second-factor-add-totp.hbs +++ b/app/assets/javascripts/discourse/templates/modal/second-factor-add-totp.hbs @@ -33,10 +33,14 @@
- - +
- {{second-factor-input maxlength=6 value=secondFactorToken inputId='second-factor-token'}} + {{second-factor-input value=secondFactorName inputId='second-factor-name' placeholder=(i18n 'user.second_factor.totp.default_name')}} +
+ + +
+ {{second-factor-input maxlength=6 value=secondFactorToken inputId='second-factor-token' placeholder='123456'}}
diff --git a/app/assets/javascripts/discourse/templates/modal/second-factor-edit-security-key.hbs b/app/assets/javascripts/discourse/templates/modal/second-factor-edit-security-key.hbs new file mode 100644 index 0000000000..7de9e5a2ad --- /dev/null +++ b/app/assets/javascripts/discourse/templates/modal/second-factor-edit-security-key.hbs @@ -0,0 +1,15 @@ +{{#d-modal-body}} +
+ {{input type="text" value=model.name}} +
+
+ {{i18n 'user.second_factor.security_key.edit_description'}} +
+ {{d-button action=(action "editSecurityKey") + class="btn-primary" + label="user.second_factor.security_key.edit"}} + + {{d-button action=(action "disableSecurityKey") + class="btn-danger" + label="user.second_factor.security_key.delete"}} +{{/d-modal-body}} diff --git a/app/assets/javascripts/discourse/templates/password-reset.hbs b/app/assets/javascripts/discourse/templates/password-reset.hbs index b145500da7..5599e20864 100644 --- a/app/assets/javascripts/discourse/templates/password-reset.hbs +++ b/app/assets/javascripts/discourse/templates/password-reset.hbs @@ -16,20 +16,33 @@ {{/if}} {{else}} - {{#if secondFactorRequired}} + {{#if securityKeyOrSecondFactorRequired }} {{#if errorMessage}}
{{errorMessage}}

{{/if}} - - {{#second-factor-form - secondFactorMethod=secondFactorMethod - secondFactorToken=secondFactorToken - backupEnabled=backupEnabled - isLogin=false}} - {{second-factor-input value=secondFactorToken inputId='second-factor' secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}} - {{/second-factor-form}} - {{d-button action=(action "submit") class='btn-primary' label='submit'}} + {{#if securityKeyRequired }} + {{#security-key-form + allowedCredentialIds=model.allowed_credential_ids + challenge=model.security_key_challenge + showSecurityKey=model.security_key_required + showSecondFactor=false + secondFactorMethod=secondFactorMethod + otherMethodAllowed=secondFactorRequired + action=(action "authenticateSecurityKey")}} + {{/security-key-form}} + {{else}} + {{#second-factor-form + secondFactorMethod=secondFactorMethod + secondFactorToken=secondFactorToken + backupEnabled=backupEnabled + isLogin=false}} + {{second-factor-input value=secondFactorToken inputId='second-factor' secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}} + {{/second-factor-form}} + {{/if}} + {{#unless securityKeyRequired }} + {{d-button action=(action "submit") class='btn-primary' label='submit'}} + {{/unless}} {{else}}

{{i18n 'user.change_password.choose'}}

diff --git a/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs b/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs index bcd2f6f48f..65100cf226 100644 --- a/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs +++ b/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs @@ -54,6 +54,33 @@ +
+
+

{{i18n "user.second_factor.security_key.title"}}

+ {{d-button action=(action "createSecurityKey") + class="btn-primary new-security-key" + disabled=loading + label="user.second_factor.security_key.add"}} + {{#each security_keys as |security_key|}} +
+ {{#if security_key.name}} + {{security_key.name}} + {{else}} + {{i18n "user.second_factor.security_key.default_name"}} + {{/if}} + + {{#if isCurrentUser}} + {{d-button action=(action "editSecurityKey" security_key) + class="btn-default btn-small btn-icon pad-left no-text edit" + disabled=loading + icon="pencil-alt" + }} + {{/if}} +
+ {{/each}} +
+
+

{{i18n "user.second_factor_backup.title"}}

@@ -99,7 +126,7 @@ {{text-field value=password id="password" type="password" - classNames="input-xxlarge" + classNames="input-large" autofocus="autofocus"}}
@@ -115,16 +142,14 @@ disabled=loading label="continue"}} - {{d-button action=(action "resetPassword") - class="btn" - disabled=resetPasswordLoading - icon="envelope" - label='user.change_password.action'}} - - {{resetPasswordProgress}} - {{#unless showEnforcedNotice}} - {{cancel-link route="preferences.account" args= model.username}} + {{cancel-link route="preferences.account" args=model.username}} + {{/unless}} +
+
+ {{resetPasswordProgress}} + {{#unless resetPasswordLoading}} + {{ i18n 'user.second_factor.forgot_password' }} {{/unless}}
diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs index c4122b52fb..9d77e4a4b5 100644 --- a/app/assets/javascripts/discourse/templates/topic.hbs +++ b/app/assets/javascripts/discourse/templates/topic.hbs @@ -1,105 +1,154 @@ {{#discourse-topic multiSelect=multiSelect enteredAt=enteredAt topic=model hasScrolled=hasScrolled}} - {{#if model.view_hidden}} - {{topic-join-group-notice model=model action=(action "joinGroup")}} - {{else}} - {{#if model}} - {{add-category-tag-classes category=model.category tags=model.tags}} -
- {{discourse-banner user=currentUser banner=site.banner overlay=hasScrolled hide=model.errorLoading}} -
- {{/if}} + {{#if model}} + {{add-category-tag-classes category=model.category tags=model.tags}} +
+ {{discourse-banner user=currentUser banner=site.banner overlay=hasScrolled hide=model.errorLoading}} +
+ {{/if}} - {{#if showSharedDraftControls}} - {{shared-draft-controls topic=model}} - {{/if}} + {{#if showSharedDraftControls}} + {{shared-draft-controls topic=model}} + {{/if}} - {{plugin-outlet name="topic-above-post-stream" args=(hash model=model)}} + {{plugin-outlet name="topic-above-post-stream" args=(hash model=model)}} - {{#if model.postStream.loaded}} - {{#if model.postStream.firstPostPresent}} - {{#topic-title cancelled=(action "cancelEditingTopic") save=(action "finishedEditingTopic") model=model}} - {{#if editingTopic}} -
- {{#if model.isPrivateMessage}} - {{d-icon "envelope"}} + {{#if model.postStream.loaded}} + {{#if model.postStream.firstPostPresent}} + {{#topic-title cancelled=(action "cancelEditingTopic") save=(action "finishedEditingTopic") model=model}} + {{#if editingTopic}} +
+ {{#if model.isPrivateMessage}} + {{d-icon "envelope"}} + {{/if}} + {{text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length autofocus="true"}} + + {{#if showCategoryChooser}} + {{category-chooser + class="small" + value=(unbound buffered.category_id) + onSelectAny=(action "topicCategoryChanged")}} {{/if}} - {{text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length autofocus="true"}} - {{#if showCategoryChooser}} - {{category-chooser - class="small" - value=(unbound buffered.category_id) - onSelectAny=(action "topicCategoryChanged")}} - {{/if}} + {{#if canEditTags}} + {{mini-tag-chooser + filterable=true + tags=(unbound buffered.tags) + categoryId=(unbound buffered.category_id) + onChangeTags=(action "topicTagsChanged")}} + {{/if}} - {{#if canEditTags}} - {{mini-tag-chooser - filterable=true - tags=(unbound buffered.tags) - categoryId=(unbound buffered.category_id) - onChangeTags=(action "topicTagsChanged")}} - {{/if}} + {{plugin-outlet name="edit-topic" args=(hash model=model buffered=buffered)}} - {{plugin-outlet name="edit-topic" args=(hash model=model buffered=buffered)}} +
+ {{d-button action=(action "finishedEditingTopic") class="btn-primary submit-edit" icon="check"}} + {{d-button action=(action "cancelEditingTopic") class="btn-default cancel-edit" icon="times"}} -
- {{d-button action=(action "finishedEditingTopic") class="btn-primary submit-edit" icon="check"}} - {{d-button action=(action "cancelEditingTopic") class="btn-default cancel-edit" icon="times"}} - - {{#if canRemoveTopicFeaturedLink}} - - {{d-icon "times-circle"}} - {{featuredLinkDomain}} - - {{/if}} -
-
- - {{else}} -

- {{#unless model.is_warning}} - {{#if siteSettings.enable_personal_messages}} - {{#if model.isPrivateMessage}} - - {{d-icon "envelope"}} - - {{/if}} - {{else}} - {{#if model.isPrivateMessage}} - {{d-icon "envelope"}} - {{/if}} - {{/if}} - {{/unless}} - - {{#if model.details.loaded}} - {{topic-status topic=model}} - - {{{model.fancyTitle}}} + {{#if canRemoveTopicFeaturedLink}} + + {{d-icon "times-circle"}} + {{featuredLinkDomain}} {{/if}} +

+
- {{#if model.details.can_edit}} - {{d-icon "pencil-alt"}} + {{else}} +

+ {{#unless model.is_warning}} + {{#if siteSettings.enable_personal_messages}} + {{#if model.isPrivateMessage}} + + {{d-icon "envelope"}} + + {{/if}} + {{else}} + {{#if model.isPrivateMessage}} + {{d-icon "envelope"}} + {{/if}} {{/if}} -

+ {{/unless}} - {{topic-category topic=model class="topic-category"}} + {{#if model.details.loaded}} + {{topic-status topic=model}} + + {{{model.fancyTitle}}} + + {{/if}} + + {{#if model.details.can_edit}} + {{d-icon "pencil-alt"}} + {{/if}} + + + {{topic-category topic=model class="topic-category"}} + {{/if}} + {{/topic-title}} + {{/if}} + + +
+
+ {{partial "selected-posts"}} +
+ + {{#topic-navigation topic=model jumpToDate=(action "jumpToDate") jumpToIndex=(action "jumpToIndex") as |info|}} + {{#if info.renderTimeline}} + {{#if info.renderAdminMenuButton}} + {{topic-admin-menu-button + topic=model + fixed="true" + toggleMultiSelect=(action "toggleMultiSelect") + deleteTopic=(action "deleteTopic") + recoverTopic=(action "recoverTopic") + toggleClosed=(action "toggleClosed") + toggleArchived=(action "toggleArchived") + toggleVisibility=(action "toggleVisibility") + showTopicStatusUpdate=(route-action "showTopicStatusUpdate") + showFeatureTopic=(route-action "showFeatureTopic") + showChangeTimestamp=(route-action "showChangeTimestamp") + resetBumpDate=(action "resetBumpDate") + convertToPublicTopic=(action "convertToPublicTopic") + convertToPrivateMessage=(action "convertToPrivateMessage")}} {{/if}} - {{/topic-title}} - {{/if}} - -
-
- {{partial "selected-posts"}} -
- - {{#topic-navigation topic=model jumpToDate=(action "jumpToDate") jumpToIndex=(action "jumpToIndex") as |info|}} - {{#if info.renderTimeline}} + {{topic-timeline + topic=model + notificationLevel=model.details.notification_level + prevEvent=info.prevEvent + fullscreen=info.topicProgressExpanded + enteredIndex=enteredIndex + loading=model.postStream.loading + jumpToPost=(action "jumpToPost") + jumpTop=(action "jumpTop") + jumpBottom=(action "jumpBottom") + jumpEnd=(action "jumpEnd") + jumpToPostPrompt=(action "jumpToPostPrompt") + jumpToIndex=(action "jumpToIndex") + replyToPost=(action "replyToPost") + toggleMultiSelect=(action "toggleMultiSelect") + deleteTopic=(action "deleteTopic") + recoverTopic=(action "recoverTopic") + toggleClosed=(action "toggleClosed") + toggleArchived=(action "toggleArchived") + toggleVisibility=(action "toggleVisibility") + showTopicStatusUpdate=(route-action "showTopicStatusUpdate") + showFeatureTopic=(route-action "showFeatureTopic") + showChangeTimestamp=(route-action "showChangeTimestamp") + resetBumpDate=(action "resetBumpDate") + convertToPublicTopic=(action "convertToPublicTopic") + convertToPrivateMessage=(action "convertToPrivateMessage")}} + {{else}} + {{#topic-progress + prevEvent=info.prevEvent + topic=model + expanded=info.topicProgressExpanded + jumpToPost=(action "jumpToPost")}} + {{plugin-outlet name="before-topic-progress" args=(hash model=model jumpToPost=(action "jumpToPost"))}} {{#if info.renderAdminMenuButton}} {{topic-admin-menu-button topic=model - fixed="true" + openUpwards="true" + rightSide="true" toggleMultiSelect=(action "toggleMultiSelect") deleteTopic=(action "deleteTopic") recoverTopic=(action "recoverTopic") @@ -113,264 +162,212 @@ convertToPublicTopic=(action "convertToPublicTopic") convertToPrivateMessage=(action "convertToPrivateMessage")}} {{/if}} + {{/topic-progress}} + {{/if}} + {{/topic-navigation}} - {{topic-timeline - topic=model - notificationLevel=model.details.notification_level - prevEvent=info.prevEvent - fullscreen=info.topicProgressExpanded - enteredIndex=enteredIndex - loading=model.postStream.loading - jumpToPost=(action "jumpToPost") - jumpTop=(action "jumpTop") - jumpBottom=(action "jumpBottom") - jumpToPostPrompt=(action "jumpToPostPrompt") - jumpToIndex=(action "jumpToIndex") - replyToPost=(action "replyToPost") - toggleMultiSelect=(action "toggleMultiSelect") - deleteTopic=(action "deleteTopic") - recoverTopic=(action "recoverTopic") - toggleClosed=(action "toggleClosed") - toggleArchived=(action "toggleArchived") - toggleVisibility=(action "toggleVisibility") - showTopicStatusUpdate=(route-action "showTopicStatusUpdate") - showFeatureTopic=(route-action "showFeatureTopic") - showChangeTimestamp=(route-action "showChangeTimestamp") - resetBumpDate=(action "resetBumpDate") - convertToPublicTopic=(action "convertToPublicTopic") - convertToPrivateMessage=(action "convertToPrivateMessage")}} - {{else}} - {{#topic-progress - prevEvent=info.prevEvent - topic=model - expanded=info.topicProgressExpanded - jumpToPost=(action "jumpToPost")}} - {{plugin-outlet name="before-topic-progress" args=(hash model=model jumpToPost=(action "jumpToPost"))}} - {{#if info.renderAdminMenuButton}} - {{topic-admin-menu-button - topic=model - openUpwards="true" - rightSide="true" - toggleMultiSelect=(action "toggleMultiSelect") - deleteTopic=(action "deleteTopic") - recoverTopic=(action "recoverTopic") - toggleClosed=(action "toggleClosed") - toggleArchived=(action "toggleArchived") - toggleVisibility=(action "toggleVisibility") - showTopicStatusUpdate=(route-action "showTopicStatusUpdate") - showFeatureTopic=(route-action "showFeatureTopic") - showChangeTimestamp=(route-action "showChangeTimestamp") - resetBumpDate=(action "resetBumpDate") - convertToPublicTopic=(action "convertToPublicTopic") - convertToPrivateMessage=(action "convertToPrivateMessage")}} - {{/if}} - {{/topic-progress}} - {{/if}} - {{/topic-navigation}} +
+
-
-
+
+ {{conditional-loading-spinner condition=model.postStream.loadingAbove}} -
- {{conditional-loading-spinner condition=model.postStream.loadingAbove}} + {{plugin-outlet name="topic-above-posts" args=(hash model=model)}} - {{plugin-outlet name="topic-above-posts" args=(hash model=model)}} + {{#unless model.postStream.loadingFilter}} + {{scrolling-post-stream + posts=postsToRender + canCreatePost=model.details.can_create_post + multiSelect=multiSelect + selectedPostsCount=selectedPostsCount + selectedQuery=selectedQuery + gaps=model.postStream.gaps + showReadIndicator=model.show_read_indicator + showFlags=(action "showPostFlags") + editPost=(action "editPost") + showHistory=(route-action "showHistory") + showLogin=(route-action "showLogin") + showRawEmail=(route-action "showRawEmail") + deletePost=(action "deletePost") + recoverPost=(action "recoverPost") + expandHidden=(action "expandHidden") + newTopicAction=(action "replyAsNewTopic") + toggleBookmark=(action "toggleBookmark") + togglePostType=(action "togglePostType") + rebakePost=(action "rebakePost") + changePostOwner=(action "changePostOwner") + grantBadge=(action "grantBadge") + addNotice=(action "addNotice") + removeNotice=(action "removeNotice") + lockPost=(action "lockPost") + unlockPost=(action "unlockPost") + unhidePost=(action "unhidePost") + replyToPost=(action "replyToPost") + toggleWiki=(action "toggleWiki") + toggleSummary=(action "toggleSummary") + removeAllowedUser=(action "removeAllowedUser") + removeAllowedGroup=(action "removeAllowedGroup") + topVisibleChanged=(action "topVisibleChanged") + currentPostChanged=(action "currentPostChanged") + currentPostScrolled=(action "currentPostScrolled") + bottomVisibleChanged=(action "bottomVisibleChanged") + togglePostSelection=(action "togglePostSelection") + selectReplies=(action "selectReplies") + selectBelow=(action "selectBelow") + fillGapBefore=(action "fillGapBefore") + fillGapAfter=(action "fillGapAfter") + showInvite=(route-action "showInvite")}} + {{/unless}} - {{#unless model.postStream.loadingFilter}} - {{scrolling-post-stream - posts=postsToRender - canCreatePost=model.details.can_create_post - multiSelect=multiSelect - selectedPostsCount=selectedPostsCount - selectedQuery=selectedQuery - gaps=model.postStream.gaps - showReadIndicator=model.show_read_indicator - showFlags=(action "showPostFlags") - editPost=(action "editPost") - showHistory=(route-action "showHistory") - showLogin=(route-action "showLogin") - showRawEmail=(route-action "showRawEmail") - deletePost=(action "deletePost") - recoverPost=(action "recoverPost") - expandHidden=(action "expandHidden") - newTopicAction=(action "replyAsNewTopic") - toggleBookmark=(action "toggleBookmark") - togglePostType=(action "togglePostType") - rebakePost=(action "rebakePost") - changePostOwner=(action "changePostOwner") - grantBadge=(action "grantBadge") - addNotice=(action "addNotice") - removeNotice=(action "removeNotice") - lockPost=(action "lockPost") - unlockPost=(action "unlockPost") - unhidePost=(action "unhidePost") - replyToPost=(action "replyToPost") - toggleWiki=(action "toggleWiki") - toggleSummary=(action "toggleSummary") - removeAllowedUser=(action "removeAllowedUser") - removeAllowedGroup=(action "removeAllowedGroup") - topVisibleChanged=(action "topVisibleChanged") - currentPostChanged=(action "currentPostChanged") - currentPostScrolled=(action "currentPostScrolled") - bottomVisibleChanged=(action "bottomVisibleChanged") - togglePostSelection=(action "togglePostSelection") - selectReplies=(action "selectReplies") - selectBelow=(action "selectBelow") - fillGapBefore=(action "fillGapBefore") - fillGapAfter=(action "fillGapAfter") - showInvite=(route-action "showInvite")}} - {{/unless}} + {{conditional-loading-spinner condition=model.postStream.loadingBelow}} +
+
- {{conditional-loading-spinner condition=model.postStream.loadingBelow}} -
-
+ {{#conditional-loading-spinner condition=model.postStream.loadingFilter}} + {{#if loadedAllPosts}} - {{#conditional-loading-spinner condition=model.postStream.loadingFilter}} - {{#if loadedAllPosts}} - - {{#if model.pending_posts}} -
- {{#each model.pending_posts as |pending|}} -
- -
- {{reviewable-created-by user=currentUser tagName=''}} -
- {{reviewable-created-by-name user=currentUser tagName=''}} -
{{cook-text pending.raw}}
-
-
-
- {{d-button - class="btn-danger" - label="review.delete" - icon="trash-alt" - action=(action "deletePending" pending) }} + {{#if model.pending_posts}} +
+ {{#each model.pending_posts as |pending|}} +
+ +
+ {{reviewable-created-by user=currentUser tagName=''}} +
+ {{reviewable-created-by-name user=currentUser tagName=''}} +
{{cook-text pending.raw}}
- {{/each}} -
- {{/if}} - - {{#if model.queued_posts_count}} -
-
- {{{i18n "review.topic_has_pending" count=model.queued_posts_count}}} +
+ {{d-button + class="btn-danger" + label="review.delete" + icon="trash-alt" + action=(action "deletePending" pending) }} +
- - {{#link-to 'review' (query-params topic_id=model.id type="ReviewableQueuedPost" status="pending")}} - {{i18n "review.view_pending"}} - {{/link-to}} -
- {{/if}} - - {{#if model.private_topic_timer.execute_at}} - {{topic-timer-info - topicClosed=model.closed - statusType=model.private_topic_timer.status_type - executeAt=model.private_topic_timer.execute_at - duration=model.private_topic_timer.duration - removeTopicTimer=(action "removeTopicTimer" model.private_topic_timer.status_type "private_topic_timer")}} - {{/if}} - - {{topic-timer-info - topicClosed=model.closed - statusType=model.topic_timer.status_type - executeAt=model.topic_timer.execute_at - basedOnLastPost=model.topic_timer.based_on_last_post - duration=model.topic_timer.duration - categoryId=model.topic_timer.category_id - removeTopicTimer=(action "removeTopicTimer" model.topic_timer.status_type "topic_timer")}} - - {{#if session.showSignupCta}} - {{! replace "Log In to Reply" with the infobox }} - {{signup-cta}} - {{else}} - {{#if currentUser}} - {{plugin-outlet name="topic-above-footer-buttons" args=(hash model=model)}} - - {{topic-footer-buttons - topic=model - toggleMultiSelect=(action "toggleMultiSelect") - deleteTopic=(action "deleteTopic") - recoverTopic=(action "recoverTopic") - toggleClosed=(action "toggleClosed") - toggleArchived=(action "toggleArchived") - toggleVisibility=(action "toggleVisibility") - showTopicStatusUpdate=(route-action "showTopicStatusUpdate") - showFeatureTopic=(route-action "showFeatureTopic") - showChangeTimestamp=(route-action "showChangeTimestamp") - resetBumpDate=(action "resetBumpDate") - convertToPublicTopic=(action "convertToPublicTopic") - convertToPrivateMessage=(action "convertToPrivateMessage") - toggleBookmark=(action "toggleBookmark") - showFlagTopic=(route-action "showFlagTopic") - toggleArchiveMessage=(action "toggleArchiveMessage") - editFirstPost=(action "editFirstPost") - deferTopic=(action "deferTopic") - replyToPost=(action "replyToPost")}} - {{else}} - - {{/if}} - {{/if}} - - {{#if showSelectedPostsAtBottom}} -
- {{partial "selected-posts"}} -
- {{/if}} - - {{plugin-outlet name="topic-above-suggested" args=(hash model=model)}} -
- {{#if model.relatedMessages.length}} - {{related-messages topic=model}} - {{/if}} - {{#if model.suggestedTopics.length}} - {{suggested-topics topic=model}} - {{/if}} + {{/each}}
{{/if}} - {{/conditional-loading-spinner}} -
-
+ {{#if model.queued_posts_count}} +
+
+ {{{i18n "review.topic_has_pending" count=model.queued_posts_count}}} +
-
- {{else}} -
- {{#conditional-loading-spinner condition=noErrorYet}} - {{#if model.notFoundHtml}} -
{{{model.notFoundHtml}}}
- {{else}} -
-
{{model.message}}
- {{#if model.noRetry}} - {{#unless currentUser}} - {{d-button action=(route-action "showLogin") class="btn-primary topic-retry" icon="user" label="log_in"}} - {{/unless}} - {{else}} - {{d-button action=(action "retryLoading") class="btn-primary topic-retry" icon="sync" label="errors.buttons.again"}} + {{#link-to 'review' (query-params topic_id=model.id type="ReviewableQueuedPost" status="pending")}} + {{i18n "review.view_pending"}} + {{/link-to}} +
{{/if}} -
- {{conditional-loading-spinner condition=retrying}} - {{/if}} - {{/conditional-loading-spinner}} + + {{#if model.private_topic_timer.execute_at}} + {{topic-timer-info + topicClosed=model.closed + statusType=model.private_topic_timer.status_type + executeAt=model.private_topic_timer.execute_at + duration=model.private_topic_timer.duration + removeTopicTimer=(action "removeTopicTimer" model.private_topic_timer.status_type "private_topic_timer")}} + {{/if}} + + {{topic-timer-info + topicClosed=model.closed + statusType=model.topic_timer.status_type + executeAt=model.topic_timer.execute_at + basedOnLastPost=model.topic_timer.based_on_last_post + duration=model.topic_timer.duration + categoryId=model.topic_timer.category_id + removeTopicTimer=(action "removeTopicTimer" model.topic_timer.status_type "topic_timer")}} + + {{#if session.showSignupCta}} + {{! replace "Log In to Reply" with the infobox }} + {{signup-cta}} + {{else}} + {{#if currentUser}} + {{plugin-outlet name="topic-above-footer-buttons" args=(hash model=model)}} + + {{topic-footer-buttons + topic=model + toggleMultiSelect=(action "toggleMultiSelect") + deleteTopic=(action "deleteTopic") + recoverTopic=(action "recoverTopic") + toggleClosed=(action "toggleClosed") + toggleArchived=(action "toggleArchived") + toggleVisibility=(action "toggleVisibility") + showTopicStatusUpdate=(route-action "showTopicStatusUpdate") + showFeatureTopic=(route-action "showFeatureTopic") + showChangeTimestamp=(route-action "showChangeTimestamp") + resetBumpDate=(action "resetBumpDate") + convertToPublicTopic=(action "convertToPublicTopic") + convertToPrivateMessage=(action "convertToPrivateMessage") + toggleBookmark=(action "toggleBookmark") + showFlagTopic=(route-action "showFlagTopic") + toggleArchiveMessage=(action "toggleArchiveMessage") + editFirstPost=(action "editFirstPost") + deferTopic=(action "deferTopic") + replyToPost=(action "replyToPost")}} + {{else}} + + {{/if}} + {{/if}} + + {{#if showSelectedPostsAtBottom}} +
+ {{partial "selected-posts"}} +
+ {{/if}} + + {{plugin-outlet name="topic-above-suggested" args=(hash model=model)}} +
+ {{#if model.relatedMessages.length}} + {{related-messages topic=model}} + {{/if}} + {{#if model.suggestedTopics.length}} + {{suggested-topics topic=model}} + {{/if}} +
+ {{/if}} + {{/conditional-loading-spinner}} + +
- {{/if}} - {{share-popup topic=model replyAsNewTopic=(action "replyAsNewTopic")}} +
+ {{else}} +
+ {{#conditional-loading-spinner condition=noErrorYet}} + {{#if model.errorHtml}} +
{{{model.errorHtml}}}
+ {{else}} +
+
{{model.errorMessage}}
+ {{#if model.noRetry}} + {{#unless currentUser}} + {{d-button action=(route-action "showLogin") class="btn-primary topic-retry" icon="user" label="log_in"}} + {{/unless}} + {{else}} + {{d-button action=(action "retryLoading") class="btn-primary topic-retry" icon="sync" label="errors.buttons.again"}} + {{/if}} +
+ {{conditional-loading-spinner condition=retrying}} + {{/if}} + {{/conditional-loading-spinner}} +
+ {{/if}} - {{#if embedQuoteButton}} - {{quote-button quoteState=quoteState selectText=(action "selectText")}} - {{/if}} + {{share-popup topic=model replyAsNewTopic=(action "replyAsNewTopic")}} + + {{#if embedQuoteButton}} + {{quote-button quoteState=quoteState selectText=(action "selectText")}} {{/if}} {{/discourse-topic}} diff --git a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 index eb14943d2e..c44035f76c 100644 --- a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 @@ -396,8 +396,7 @@ export default createWidget("post-menu", { }, menuItems() { - let result = this.siteSettings.post_menu.split("|"); - return result; + return this.siteSettings.post_menu.split("|").filter(Boolean); }, html(attrs, state) { @@ -526,6 +525,19 @@ export default createWidget("post-menu", { ) ]; + if (state.readers.length) { + const remaining = state.totalReaders - state.readers.length; + contents.push( + this.attach("small-user-list", { + users: state.readers, + addSelf: false, + listClassName: "who-read", + description: "post.actions.people.read", + count: remaining + }) + ); + } + if (state.likedUsers.length) { const remaining = state.total - state.likedUsers.length; contents.push( @@ -542,19 +554,6 @@ export default createWidget("post-menu", { ); } - if (state.readers.length) { - const remaining = state.totalReaders - state.readers.length; - contents.push( - this.attach("small-user-list", { - users: state.readers, - addSelf: false, - listClassName: "who-read", - description: "post.actions.people.read", - count: remaining - }) - ); - } - return contents; }, diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6 index 65f55a996b..12eadf99cb 100644 --- a/app/assets/javascripts/discourse/widgets/post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post.js.es6 @@ -692,9 +692,10 @@ export default createWidget("post", { const likeAction = post.get("likeAction"); if (likeAction && likeAction.get("canToggle")) { - return likeAction - .togglePromise(post) - .then(result => this._warnIfClose(result)); + return likeAction.togglePromise(post).then(result => { + this.appEvents.trigger("page:like-toggled", post, likeAction); + return this._warnIfClose(result); + }); } }, diff --git a/app/assets/javascripts/discourse/widgets/quick-access-profile.js.es6 b/app/assets/javascripts/discourse/widgets/quick-access-profile.js.es6 index b74054e243..4fc9140b3f 100644 --- a/app/assets/javascripts/discourse/widgets/quick-access-profile.js.es6 +++ b/app/assets/javascripts/discourse/widgets/quick-access-profile.js.es6 @@ -45,6 +45,11 @@ createWidgetFrom(QuickAccessPanel, "quick-access-profile", { href: `${this.attrs.path}/messages`, content: I18n.t("user.private_messages") }, + { + icon: "pencil", + href: `${this.attrs.path}/activity/drafts`, + content: I18n.t("user_action_groups.15") + }, { icon: "cog", href: `${this.attrs.path}/preferences`, diff --git a/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 b/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 index ef8bfed2e3..eac0ac7b5c 100644 --- a/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 @@ -148,7 +148,8 @@ createWidget("timeline-scrollarea", { const postStream = topic.get("postStream"); const total = postStream.get("filteredPostsCount"); - const current = clamp(Math.floor(total * percentage) + 1, 1, total); + const scrollPosition = clamp(Math.floor(total * percentage), 0, total) + 1; + const current = clamp(scrollPosition, 1, total); const daysAgo = postStream.closestDaysAgoFor(current); let date; @@ -168,6 +169,7 @@ createWidget("timeline-scrollarea", { const result = { current, + scrollPosition, total, date, lastRead: null, @@ -183,9 +185,13 @@ createWidget("timeline-scrollarea", { result.lastReadPercentage = this._percentFor(topic, idx); } - if (this.state.position !== result.current) { - this.state.position = result.current; - this.sendWidgetAction("updatePosition", result.current); + if (this.state.position !== result.scrollPosition) { + this.state.position = result.scrollPosition; + this.sendWidgetAction( + "updatePosition", + result.position, + result.scrollPosition + ); } return result; @@ -259,7 +265,11 @@ createWidget("timeline-scrollarea", { const position = this.position(); this.state.scrolledPost = position.current; - this.sendWidgetAction("jumpToIndex", position.current); + if (position.current === position.scrollPosition) { + this.sendWidgetAction("jumpToIndex", position.current); + } else { + this.sendWidgetAction("jumpEnd"); + } }, topicCurrentPostScrolled(event) { @@ -380,25 +390,25 @@ export default createWidget("topic-timeline", { return { position: null, excerpt: null }; }, - updatePosition(pos) { + updatePosition(postIdx, scrollPosition) { if (!this.attrs.fullScreen) { return; } - this.state.position = pos; + this.state.position = scrollPosition; this.state.excerpt = ""; const stream = this.attrs.topic.get("postStream"); // a little debounce to avoid flashing Ember.run.later(() => { - if (!this.state.position === pos) { + if (!this.state.position === scrollPosition) { return; } // we have an off by one, stream is zero based, - // pos is 1 based - stream.excerpt(pos - 1).then(info => { - if (info && this.state.position === pos) { + // postIdx is 1 based + stream.excerpt(postIdx - 1).then(info => { + if (info && this.state.position === scrollPosition) { let excerpt = ""; if (info.username) { diff --git a/app/assets/javascripts/discourse/widgets/widget.js.es6 b/app/assets/javascripts/discourse/widgets/widget.js.es6 index 6888d66209..f6f5f78b91 100644 --- a/app/assets/javascripts/discourse/widgets/widget.js.es6 +++ b/app/assets/javascripts/discourse/widgets/widget.js.es6 @@ -111,7 +111,7 @@ export default class Widget { this.currentUser = register.lookup("current-user:main"); this.capabilities = register.lookup("capabilities:main"); this.store = register.lookup("service:store"); - this.appEvents = register.lookup("app-events:main"); + this.appEvents = register.lookup("service:app-events"); this.keyValueStore = register.lookup("key-value-store:main"); // Helps debug widgets diff --git a/app/assets/javascripts/omniauth-complete.js.no-module.es6 b/app/assets/javascripts/omniauth-complete.js.no-module.es6 deleted file mode 100644 index 7be373d50f..0000000000 --- a/app/assets/javascripts/omniauth-complete.js.no-module.es6 +++ /dev/null @@ -1,18 +0,0 @@ -(function() { - const { authResult, baseUrl } = document.getElementById( - "data-auth-result" - ).dataset; - const parsedAuthResult = JSON.parse(authResult); - - if ( - !window.opener || - !window.opener.Discourse || - !window.opener.Discourse.authenticationComplete - ) { - localStorage.setItem("lastAuthResult", authResult); - window.location.href = `${baseUrl}?authComplete=true`; - } else { - window.opener.Discourse.authenticationComplete(parsedAuthResult); - window.close(); - } -})(); diff --git a/app/assets/javascripts/service-worker.js.erb b/app/assets/javascripts/service-worker.js.erb index a2df90a506..1d88932289 100644 --- a/app/assets/javascripts/service-worker.js.erb +++ b/app/assets/javascripts/service-worker.js.erb @@ -7,7 +7,7 @@ workbox.setConfig({ debug: false }); -const cacheVersion = "1"; +var cacheVersion = "1"; // Cache all GET requests, so Discourse can be used while offline workbox.routing.registerRoute( @@ -24,7 +24,7 @@ workbox.routing.registerRoute( }) ); -const idleThresholdTime = 1000 * 10; // 10 seconds +var idleThresholdTime = 1000 * 10; // 10 seconds var lastAction = -1; function isIdle() { diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index c815aade5b..9b72049a89 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -100,10 +100,10 @@ $mobile-breakpoint: 700px; padding: 8px; } tr:hover { - background-color: darken($secondary, 2.5%); + background-color: $primary-very-low; } tr.selected { - background-color: lighten($primary, 80%); + background-color: $primary-low; } .filters input { margin-bottom: 0; @@ -341,8 +341,8 @@ $mobile-breakpoint: 700px; } .admin-users .users-list { - .username .fa { - color: dark-light-choose($primary-medium, $secondary-medium); + .username .d-icon { + color: $primary-medium; } } @@ -566,12 +566,12 @@ $mobile-breakpoint: 700px; float: left; padding: 5px 10px; margin-right: 15px; - border: 1px solid lighten($primary, 40%); + border: 1px solid $primary-medium; border-radius: 3px; background: transparent; color: $primary; &:hover { - background-color: lighten($primary, 60%); + background-color: $primary-low-mid; } @media (max-width: $mobile-breakpoint) { display: inline-block; @@ -659,7 +659,7 @@ $mobile-breakpoint: 700px; } .text-muted { - color: lighten($primary, 40); + color: $primary-medium; } .admin-nav { diff --git a/app/assets/stylesheets/common/admin/api.scss b/app/assets/stylesheets/common/admin/api.scss index bddfdfcda4..e2a921d585 100644 --- a/app/assets/stylesheets/common/admin/api.scss +++ b/app/assets/stylesheets/common/admin/api.scss @@ -167,12 +167,12 @@ table.api-keys { > p { padding-bottom: 10px; - border-bottom: darken($secondary, 10%) 1px solid; + border-bottom: $primary-low 1px solid; } .filters { margin: 5px 0; padding-bottom: 5px; - border-bottom: darken($secondary, 5%) 1px solid; + border-bottom: $primary-low 1px solid; .filter { margin-bottom: 1em; } diff --git a/app/assets/stylesheets/common/admin/badges.scss b/app/assets/stylesheets/common/admin/badges.scss index 5351533f4f..57df3636bd 100644 --- a/app/assets/stylesheets/common/admin/badges.scss +++ b/app/assets/stylesheets/common/admin/badges.scss @@ -162,30 +162,3 @@ } } } - -// mobile specific styles - mostly commmon style overrides -// TODO move to mobile sheet once mobile view has a seprate template. -.mobile-view { - .admin-badges { - .badges { - margin: 0 0.25em; - } - .content-list { - flex: 0 0 100%; - .admin-badge-list { - max-height: 40vh; - margin-right: 0; - } - } - .badge-intro { - flex: 0 1 75%; - } - .current-badge { - margin: 20px 0; - width: 100%; - } - input[type="text"] { - width: 100%; - } - } -} diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index 11c3a5ccfa..8ae3c50d58 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -99,9 +99,9 @@ margin-top: 20px; } .color-schemes li { - .fa { - margin-right: 6px; - color: dark-light-choose($primary-medium, $secondary-medium); + .d-icon { + margin-right: 0.25em; + color: $primary-medium; } } .show-current-style { @@ -492,8 +492,14 @@ .hex { text-align: center; } + h3 { + margin: 0; + } .description { - color: dark-light-choose($primary-medium, $secondary-medium); + margin: 0.15em 0 0; + color: $primary-high; + font-size: $font-down-1; + line-height: $line-height-medium; } .invalid .hex input { @@ -594,7 +600,7 @@ .permalink-search { text-align: left; - @media screen and (min-width: map-get($breakpoints, tablet)) { + @include breakpoint(tablet, min-width) { text-align: right; } } @@ -604,7 +610,7 @@ display: flex; flex-direction: column; flex-wrap: wrap; - @media screen and (min-width: map-get($breakpoints, tablet)) { + @include breakpoint(tablet, min-width) { align-items: center; flex-direction: row; } @@ -614,7 +620,7 @@ } input { margin: 5px 0; - @media screen and (min-width: map-get($breakpoints, tablet)) { + @include breakpoint(tablet, min-width) { margin: 0 5px; } } @@ -625,7 +631,7 @@ } .permalink-description { - color: dark-light-choose($primary-medium, $secondary-medium); + color: $primary-medium; } // embedding @@ -705,7 +711,7 @@ margin: 0.75em 0; } p.description { - color: dark-light-choose($primary-medium, $secondary-medium); + color: $primary-medium; margin-bottom: 1em; max-width: 700px; } diff --git a/app/assets/stylesheets/common/admin/emails.scss b/app/assets/stylesheets/common/admin/emails.scss index c8903be852..cb71da61aa 100644 --- a/app/assets/stylesheets/common/admin/emails.scss +++ b/app/assets/stylesheets/common/admin/emails.scss @@ -11,7 +11,7 @@ .reply-key { display: block; font-size: $font-down-1; - color: dark-light-choose($primary-medium, $secondary-high); + color: $primary-medium; } .username div { max-width: 180px; @@ -39,7 +39,7 @@ margin: 5px 10px; } .error-description { - color: #919191; + color: $primary-medium; font-size: $font-down-1; } hr { @@ -66,7 +66,7 @@ .admin-list-item { width: 100%; - border-top: 1px solid #e9e9e9; + border-top: 1px solid $primary-low; padding: 0.25em 0; } diff --git a/app/assets/stylesheets/common/admin/settings.scss b/app/assets/stylesheets/common/admin/settings.scss index 2ec2bd3d4f..1c0f902e04 100644 --- a/app/assets/stylesheets/common/admin/settings.scss +++ b/app/assets/stylesheets/common/admin/settings.scss @@ -71,7 +71,7 @@ position: relative; line-height: $line-height-small; cursor: default; - border: 1px dashed #aaa; + border: 1px dashed $primary-low-mid; border-radius: 3px; background-clip: padding-box; -moz-user-select: none; diff --git a/app/assets/stylesheets/common/admin/staff_logs.scss b/app/assets/stylesheets/common/admin/staff_logs.scss index f28b454661..058fcb4543 100644 --- a/app/assets/stylesheets/common/admin/staff_logs.scss +++ b/app/assets/stylesheets/common/admin/staff_logs.scss @@ -1,7 +1,7 @@ // Styles for /admin/logs .web-hook-events { - border-bottom: dotted 1px dark-light-choose($primary-low-mid, $secondary); + border-bottom: dotted 1px $primary-low-mid; .heading-container { width: 100%; background-color: $primary-low; @@ -399,10 +399,10 @@ table.screened-ip-addresses { cursor: pointer; .d-icon { margin-right: 0.25em; - color: dark-light-diff($primary, $secondary, 50%, -50%); + color: $primary-medium; } &:hover .d-icon { - color: $primary; + color: $danger; } } diff --git a/app/assets/stylesheets/common/admin/users.scss b/app/assets/stylesheets/common/admin/users.scss index b150c27814..87bf9bffa9 100644 --- a/app/assets/stylesheets/common/admin/users.scss +++ b/app/assets/stylesheets/common/admin/users.scss @@ -94,8 +94,8 @@ margin-bottom: 0; } .users-list { - .username .fa { - color: dark-light-choose($primary-medium, $secondary-medium); + .username .d-icon { + color: $primary-medium; } } } diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index 2a4cab7632..ff516a3c3a 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -483,3 +483,19 @@ div.ac-wrap { opacity: 0; } } + +body.ios-safari-composer-hacks { + #main-outlet, + header, + .grippie, + html:not(.fullscreen-composer) & .toggle-fullscreen { + display: none; + } + + #reply-control { + top: 0px; + &.open { + height: calc(var(--composer-vh, 1vh) * 100); + } + } +} diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 31425a7541..d436650af9 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -120,6 +120,16 @@ button { } } +a.cancel { + margin-left: 1.25em; + line-height: normal; + color: $primary-high; + transition: color 250ms; + &:hover { + color: $danger; + } +} + ul.breadcrumb { margin: 0 10px 0 10px; } diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 44f1d6be7d..3b32d81874 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -85,6 +85,11 @@ } } + .categories-link { + display: block; + padding: 0.25em 0.5em; + } + li.category-link { float: left; background-color: transparent; diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss index d03345f56d..bf181bfc70 100644 --- a/app/assets/stylesheets/common/base/modal.scss +++ b/app/assets/stylesheets/common/base/modal.scss @@ -143,7 +143,10 @@ &:not(.history-modal) { .modal-body:not(.reorder-categories):not(.poll-ui-builder) { - max-height: none !important; + max-height: 80vh !important; + @media screen and (max-height: 500px) { + max-height: 65vh !important; + } } } } @@ -196,6 +199,12 @@ &.full-height-modal { max-height: calc(100vh - 150px); } + + &.insert-link { + input { + min-width: 300px; + } + } textarea { width: 99%; height: 80px; diff --git a/app/assets/stylesheets/common/base/not-found.scss b/app/assets/stylesheets/common/base/not-found.scss index 1cde53d2eb..da46e2151f 100644 --- a/app/assets/stylesheets/common/base/not-found.scss +++ b/app/assets/stylesheets/common/base/not-found.scss @@ -1,12 +1,13 @@ // Page not found styles -h1.page-not-found { - font-size: $font-up-5; - line-height: $line-height-medium; -} - .page-not-found { margin: 0 0 40px 0; + + h1.title { + font-size: $font-up-5; + line-height: $line-height-medium; + } + &-search { margin-top: 20px; } diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss index b00f53fb83..e707724b4d 100644 --- a/app/assets/stylesheets/common/base/onebox.scss +++ b/app/assets/stylesheets/common/base/onebox.scss @@ -378,6 +378,89 @@ pre.onebox code { white-space: normal; } +// Onebox - Github - PR, Commit & Issue +.onebox.githubpullrequest, +.onebox.githubcommit, +.onebox.githubissue { + .onebox-body { + h4 { + margin-bottom: 5px; + } + + > .github-icon { + // For backwards compatibility with old onebox + float: left; + margin-right: 5px; + } + } + + .github-row { + display: flex; + } + + .github-icon-container { + display: flex; + align-items: flex-start; + margin-right: 5px; + } + + .github-icon { + fill: $primary-high; + width: 40px; + height: 40px; + } + + .branches { + font-size: $font-down-1; + } + + .github-info { + display: flex; + align-items: center; + flex-wrap: wrap; + + > div { + margin: 2.5px 0; + } + + > div:not(:last-child) { + margin-right: 15px; + } + + .lines { + font-weight: bold; + + .added { + color: $success; + } + .removed { + color: $danger; + } + } + } + + .onebox-avatar-inline { + height: 20px; + width: 20px; + border-radius: 2px; + float: none; + margin: 0; + vertical-align: middle; + max-width: none; + } + + .github-content { + margin: 5px 0 0 0; + } + + .labels span { + // !important required to override inline style attribute + background-color: $primary-medium !important; + color: $secondary !important; + padding: 2px 4px !important; + } +} + //Onebox - Github - Pull request .onebox-body .github-commit-status { background: #f5f5f5; diff --git a/app/assets/stylesheets/common/components/footer-nav.scss b/app/assets/stylesheets/common/components/footer-nav.scss index 01b6856825..3c8d172edb 100644 --- a/app/assets/stylesheets/common/components/footer-nav.scss +++ b/app/assets/stylesheets/common/components/footer-nav.scss @@ -78,6 +78,7 @@ body.footer-nav-ipad { padding-bottom: 0; // resets safe-area-inset-bottom } + #reply-control, #reply-control.fullscreen { z-index: z("ipad-header-nav") + 1; } diff --git a/app/assets/stylesheets/common/d-editor.scss b/app/assets/stylesheets/common/d-editor.scss index 4937bc71fc..89aa1e9221 100644 --- a/app/assets/stylesheets/common/d-editor.scss +++ b/app/assets/stylesheets/common/d-editor.scss @@ -4,43 +4,12 @@ max-width: 100%; } -.d-editor-overlay { - position: absolute; - background-color: black; - opacity: 0.8; - z-index: z("modal", "overlay"); -} - -.d-editor-modals { - position: absolute; - z-index: z("modal", "content"); -} - .d-editor { display: flex; flex-grow: 1; min-height: 0; } -.d-editor .d-editor-modal { - min-width: 400px; - @media screen and (max-width: 424px) { - min-width: 300px; - } - position: absolute; - background-color: $secondary; - border: 1px solid $primary-low; - padding: 1em; - top: 25px; - - input { - width: 95%; - } - h3 { - margin-bottom: 0.5em; - } -} - .d-editor-textarea-wrapper, .d-editor-preview-wrapper { flex: 1; diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss index 386f7b44eb..7f4db285c2 100644 --- a/app/assets/stylesheets/desktop/compose.scss +++ b/app/assets/stylesheets/desktop/compose.scss @@ -282,9 +282,3 @@ a.toggle-preview { } } } - -.fullscreen-composer.keyboard-visible { - #reply-control.fullscreen { - top: 0px; - } -} diff --git a/app/assets/stylesheets/desktop/login.scss b/app/assets/stylesheets/desktop/login.scss index 2f28e06105..40b59d5b68 100644 --- a/app/assets/stylesheets/desktop/login.scss +++ b/app/assets/stylesheets/desktop/login.scss @@ -33,10 +33,11 @@ form { min-width: 300px; + max-width: 100%; } #modal-alert { - max-width: 500px; + max-width: 100%; padding: s(2 4); } diff --git a/app/assets/stylesheets/desktop/modal.scss b/app/assets/stylesheets/desktop/modal.scss index 861c62442d..9a2adb962b 100644 --- a/app/assets/stylesheets/desktop/modal.scss +++ b/app/assets/stylesheets/desktop/modal.scss @@ -44,6 +44,12 @@ .category-chooser { width: 50%; } + + .modal-body.insert-link { + input { + min-width: 450px; + } + } } .edit-category-modal { diff --git a/app/assets/stylesheets/mobile.scss b/app/assets/stylesheets/mobile.scss index a2158fe44a..567a051794 100644 --- a/app/assets/stylesheets/mobile.scss +++ b/app/assets/stylesheets/mobile.scss @@ -23,6 +23,7 @@ @import "mobile/ring"; @import "mobile/group"; @import "mobile/dashboard"; +@import "mobile/admin_badges"; @import "mobile/admin_customize"; @import "mobile/admin_reports"; @import "mobile/admin_report"; diff --git a/app/assets/stylesheets/mobile/admin_badges.scss b/app/assets/stylesheets/mobile/admin_badges.scss new file mode 100644 index 0000000000..b3f1d0ca55 --- /dev/null +++ b/app/assets/stylesheets/mobile/admin_badges.scss @@ -0,0 +1,27 @@ +.mobile-view { + .admin-badges { + .badges { + margin: 0 0.25em; + .content-list { + flex: 0 0 100%; + .admin-badge-list { + max-height: 40vh; + margin-right: 0; + } + } + } + .badge-intro { + flex: 0 1 75%; + } + .current-badge { + margin: 20px 0; + width: 100%; + textarea { + width: 100%; + } + } + input[type="text"] { + width: 100%; + } + } +} diff --git a/app/assets/stylesheets/mobile/compose.scss b/app/assets/stylesheets/mobile/compose.scss index 4c8c05bf03..2f25202bbc 100644 --- a/app/assets/stylesheets/mobile/compose.scss +++ b/app/assets/stylesheets/mobile/compose.scss @@ -21,11 +21,10 @@ height: 250px; &.edit-title { height: 100%; - height: calc(var(--composer-vh, 1vh) * 100); } } - html.keyboard-visible &.open { + body.ios-safari-composer-hacks &.open { height: calc(var(--composer-vh, 1vh) * 100); .reply-area { padding-bottom: 0px; diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index d7c1935b05..42c630652e 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -244,6 +244,8 @@ } .user-preferences { + padding-bottom: 2em; + .instructions { margin-top: s(1); } diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 8856a377b4..fe2d3a2327 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'rate_limiter' - class AboutController < ApplicationController requires_login only: [:live_post_counts] diff --git a/app/controllers/admin/backups_controller.rb b/app/controllers/admin/backups_controller.rb index 2fae2e40da..2e4762c05f 100644 --- a/app/controllers/admin/backups_controller.rb +++ b/app/controllers/admin/backups_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "backup_restore/backup_restore" +require "backup_restore" require "backup_restore/backup_store" class Admin::BackupsController < Admin::AdminController diff --git a/app/controllers/admin/email_controller.rb b/app/controllers/admin/email_controller.rb index e8c81111fa..db89c25ebc 100644 --- a/app/controllers/admin/email_controller.rb +++ b/app/controllers/admin/email_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'email/renderer' - class Admin::EmailController < Admin::AdminController def index diff --git a/app/controllers/admin/embedding_controller.rb b/app/controllers/admin/embedding_controller.rb index e2559fb85a..c4540edd80 100644 --- a/app/controllers/admin/embedding_controller.rb +++ b/app/controllers/admin/embedding_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'embedding' - class Admin::EmbeddingController < Admin::AdminController before_action :fetch_embedding diff --git a/app/controllers/admin/emojis_controller.rb b/app/controllers/admin/emojis_controller.rb index f8baa5dc39..cc74777778 100644 --- a/app/controllers/admin/emojis_controller.rb +++ b/app/controllers/admin/emojis_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'upload_creator' - class Admin::EmojisController < Admin::AdminController def index diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index fe99dd706a..20b97a6f25 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'report' - class Admin::ReportsController < Admin::AdminController def index reports_methods = ['page_view_total_reqs'] + diff --git a/app/controllers/admin/screened_ip_addresses_controller.rb b/app/controllers/admin/screened_ip_addresses_controller.rb index ac3b1b8b98..400620c4cc 100644 --- a/app/controllers/admin/screened_ip_addresses_controller.rb +++ b/app/controllers/admin/screened_ip_addresses_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'ip_addr' - class Admin::ScreenedIpAddressesController < Admin::AdminController before_action :fetch_screened_ip_address, only: [:update, :destroy] diff --git a/app/controllers/admin/site_texts_controller.rb b/app/controllers/admin/site_texts_controller.rb index 14743c3c5a..756974c109 100644 --- a/app/controllers/admin/site_texts_controller.rb +++ b/app/controllers/admin/site_texts_controller.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require_dependency 'seed_data/categories' -require_dependency 'seed_data/topics' - class Admin::SiteTextsController < Admin::AdminController def self.preferred_keys diff --git a/app/controllers/admin/staff_action_logs_controller.rb b/app/controllers/admin/staff_action_logs_controller.rb index 9926c3ce1e..5ab3fbe4d0 100644 --- a/app/controllers/admin/staff_action_logs_controller.rb +++ b/app/controllers/admin/staff_action_logs_controller.rb @@ -26,8 +26,6 @@ class Admin::StaffActionLogsController < Admin::AdminController end def diff - require_dependency "discourse_diff" - @history = UserHistory.find(params[:id]) prev = @history.previous_value cur = @history.new_value diff --git a/app/controllers/admin/themes_controller.rb b/app/controllers/admin/themes_controller.rb index b9b34aff74..9c16a0a068 100644 --- a/app/controllers/admin/themes_controller.rb +++ b/app/controllers/admin/themes_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'upload_creator' -require_dependency 'theme_store/tgz_exporter' require 'base64' class Admin::ThemesController < Admin::AdminController @@ -94,7 +92,7 @@ class Admin::ThemesController < Admin::AdminController theme_id = params[:theme_id] match_theme_by_name = !!params[:bundle] && !params.key?(:theme_id) # Old theme CLI behavior, match by name. Remove Jan 2020 begin - @theme = RemoteTheme.update_tgz_theme(bundle.path, match_theme: match_theme_by_name, user: theme_user, theme_id: theme_id) + @theme = RemoteTheme.update_zipped_theme(bundle.path, bundle.original_filename, match_theme: match_theme_by_name, user: theme_user, theme_id: theme_id) log_theme_change(nil, @theme) render json: @theme, status: :created rescue RemoteTheme::ImportError => e @@ -244,7 +242,7 @@ class Admin::ThemesController < Admin::AdminController @theme = Theme.find_by(id: params[:id]) raise Discourse::InvalidParameters.new(:id) unless @theme - exporter = ThemeStore::TgzExporter.new(@theme) + exporter = ThemeStore::ZipExporter.new(@theme) file_path = exporter.package_filename headers['Content-Length'] = File.size(file_path).to_s diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 59b9f03714..8c860a4d89 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -1,9 +1,5 @@ # frozen_string_literal: true -require_dependency 'user_destroyer' -require_dependency 'admin_user_index_query' -require_dependency 'admin_confirmation' - class Admin::UsersController < Admin::AdminController before_action :fetch_user, only: [:suspend, diff --git a/app/controllers/admin/versions_controller.rb b/app/controllers/admin/versions_controller.rb index 94ec89be68..f8da407745 100644 --- a/app/controllers/admin/versions_controller.rb +++ b/app/controllers/admin/versions_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'discourse_updates' - class Admin::VersionsController < Admin::AdminController def show render json: DiscourseUpdates.check_version diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7940565ea5..26a1bdf175 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,20 +1,6 @@ # frozen_string_literal: true require 'current_user' -require_dependency 'canonical_url' -require_dependency 'discourse' -require_dependency 'custom_renderer' -require_dependency 'archetype' -require_dependency 'rate_limiter' -require_dependency 'crawler_detection' -require_dependency 'json_error' -require_dependency 'letter_avatar' -require_dependency 'distributed_cache' -require_dependency 'global_path' -require_dependency 'secure_session' -require_dependency 'topic_query' -require_dependency 'hijack' -require_dependency 'read_only_header' class ApplicationController < ActionController::Base include CurrentUser @@ -112,15 +98,59 @@ class ApplicationController < ActionController::Base use_crawler_layout? ? 'crawler' : 'application' end - # Some exceptions class RenderEmpty < StandardError; end + class PluginDisabled < StandardError; end - # Render nothing rescue_from RenderEmpty do render 'default/empty' end - def render_rate_limit_error(e) + rescue_from ArgumentError do |e| + if e.message == "string contains null byte" + raise Discourse::InvalidParameters, e.message + else + raise e + end + end + + rescue_from PG::ReadOnlySqlTransaction do |e| + Discourse.received_postgres_readonly! + Rails.logger.error("#{e.class} #{e.message}: #{e.backtrace.join("\n")}") + raise Discourse::ReadOnly + end + + rescue_from ActionController::ParameterMissing do |e| + render_json_error e.message, status: 400 + end + + rescue_from ActionController::RoutingError, PluginDisabled do + rescue_discourse_actions(:not_found, 404) + end + + # Handles requests for giant IDs that throw pg exceptions + rescue_from ActiveModel::RangeError do |e| + if e.message =~ /ActiveModel::Type::Integer/ + rescue_discourse_actions(:not_found, 404) + else + raise e + end + end + + rescue_from ActiveRecord::RecordInvalid do |e| + if request.format && request.format.json? + render_json_error e, type: :record_invalid, status: 422 + else + raise e + end + end + + rescue_from ActiveRecord::StatementInvalid do |e| + Discourse.reset_active_record_cache_if_needed(e) + raise e + end + + # If they hit the rate limiter + rescue_from RateLimiter::LimitExceeded do |e| retry_time_in_seconds = e&.available_in render_json_error( @@ -132,25 +162,6 @@ class ApplicationController < ActionController::Base ) end - rescue_from ActiveRecord::RecordInvalid do |e| - if request.format && request.format.json? - render_json_error e, type: :record_invalid, status: 422 - else - raise e - end - end - - # If they hit the rate limiter - rescue_from RateLimiter::LimitExceeded do |e| - render_rate_limit_error(e) - end - - rescue_from PG::ReadOnlySqlTransaction do |e| - Discourse.received_postgres_readonly! - Rails.logger.error("#{e.class} #{e.message}: #{e.backtrace.join("\n")}") - raise Discourse::ReadOnly - end - rescue_from Discourse::NotLoggedIn do |e| if (request.format && request.format.json?) || request.xhr? || !request.get? rescue_discourse_actions(:not_logged_in, 403, include_ember: true) @@ -159,14 +170,6 @@ class ApplicationController < ActionController::Base end end - rescue_from ArgumentError do |e| - if e.message == "string contains null byte" - raise Discourse::InvalidParameters, e.message - else - raise e - end - end - rescue_from Discourse::InvalidParameters do |e| message = I18n.t('invalid_params', message: e.message) if (request.format && request.format.json?) || request.xhr? || !request.get? @@ -176,45 +179,27 @@ class ApplicationController < ActionController::Base end end - rescue_from ActiveRecord::StatementInvalid do |e| - Discourse.reset_active_record_cache_if_needed(e) - raise e - end - - class PluginDisabled < StandardError; end - - # Handles requests for giant IDs that throw pg exceptions - rescue_from ActiveModel::RangeError do |e| - if e.message =~ /ActiveModel::Type::Integer/ - rescue_discourse_actions(:not_found, 404) - else - raise e - end - end - rescue_from Discourse::NotFound do |e| rescue_discourse_actions( :not_found, e.status, check_permalinks: e.check_permalinks, - original_path: e.original_path + original_path: e.original_path, + custom_message: e.custom_message ) end - rescue_from PluginDisabled, ActionController::RoutingError do - rescue_discourse_actions(:not_found, 404) - end - rescue_from Discourse::InvalidAccess do |e| - if e.opts[:delete_cookie].present? cookies.delete(e.opts[:delete_cookie]) end + rescue_discourse_actions( :invalid_access, 403, include_ember: true, - custom_message: e.custom_message + custom_message: e.custom_message, + group: e.group ) end @@ -222,10 +207,6 @@ class ApplicationController < ActionController::Base render_json_error I18n.t('read_only_mode_enabled'), type: :read_only, status: 503 end - rescue_from ActionController::ParameterMissing do |e| - render_json_error e.message, status: 400 - end - def redirect_with_client_support(url, options) if request.xhr? response.headers['Discourse-Xhr-Redirect'] = 'true' @@ -255,18 +236,21 @@ class ApplicationController < ActionController::Base end message = opts[:custom_message_translated] || I18n.t(opts[:custom_message] || type) + error_page_opts = { + title: opts[:custom_message_translated] || I18n.t(opts[:custom_message] || "page_not_found.title"), + status: status_code, + group: opts[:group] + } if show_json_errors - # HACK: do not use render_json_error for topics#show - if request.params[:controller] == 'topics' && request.params[:action] == 'show' - return render( - status: status_code, - layout: false, - plain: (status_code == 404 || status_code == 410) ? build_not_found_page(status_code) : message - ) + opts = { type: type, status: status_code } + + # Include error in HTML format for topics#show. + if (request.params[:controller] == 'topics' && request.params[:action] == 'show') || (request.params[:controller] == 'categories' && request.params[:action] == 'find_by_slug') + opts[:extras] = { html: build_not_found_page(error_page_opts) } end - render_json_error message, type: type, status: status_code + render_json_error message, opts else begin # 404 pages won't have the session and theme_keys without these: @@ -276,7 +260,8 @@ class ApplicationController < ActionController::Base return render plain: message, status: status_code end - render html: build_not_found_page(status_code, opts[:include_ember] ? 'application' : 'no_ember') + error_page_opts[:layout] = opts[:include_ember] ? 'application' : 'no_ember' + render html: build_not_found_page(error_page_opts) end end @@ -738,9 +723,6 @@ class ApplicationController < ActionController::Base session[:destination_url] = destination_url redirect_to path('/session/sso') return - elsif params[:authComplete].present? - redirect_to path("/login?authComplete=true") - return else # save original URL in a cookie (javascript redirects after login in this case) cookies[:destination_url] = destination_url @@ -771,13 +753,13 @@ class ApplicationController < ActionController::Base raise Discourse::ReadOnly.new if !(request.get? || request.head?) && @readonly_mode end - def build_not_found_page(status = 404, layout = false) + def build_not_found_page(opts = {}) if SiteSetting.bootstrap_error_pages? preload_json - layout = 'application' if layout == 'no_ember' + opts[:layout] = 'application' if opts[:layout] == 'no_ember' end - if !SiteSetting.login_required? || current_user + if !SiteSetting.login_required? || (current_user rescue false) key = "page_not_found_topics" if @topics_partial = $redis.get(key) @topics_partial = @topics_partial.html_safe @@ -791,9 +773,12 @@ class ApplicationController < ActionController::Base end @container_class = "wrap not-found-container" - @slug = (params[:slug].presence || params[:id].presence || "").tr('-', '') + @title = opts[:title] || I18n.t("page_not_found.title") + @group = opts[:group] @hide_search = true if SiteSetting.login_required - render_to_string status: status, layout: layout, formats: [:html], template: '/exceptions/not_found' + @slug = (params[:slug].presence || params[:id].presence || "").tr('-', ' ') + + render_to_string status: opts[:status], layout: opts[:layout], formats: [:html], template: '/exceptions/not_found' end def is_asset_path diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index 774171395d..6f60b09cff 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'category_serializer' - class CategoriesController < ApplicationController requires_login except: [:index, :categories_and_latest, :categories_and_top, :show, :redirect, :find_by_slug] @@ -208,7 +206,18 @@ class CategoriesController < ApplicationController def find_by_slug params.require(:category_slug) @category = Category.find_by_slug(params[:category_slug], params[:parent_category_slug]) - guardian.ensure_can_see!(@category) + if !guardian.can_see?(@category) + if SiteSetting.detailed_404 && group = @category.access_category_via_group + raise Discourse::InvalidAccess.new( + 'not in group', + @category, + custom_message: 'not_in_group.title_category', + group: group + ) + else + raise Discourse::NotFound + end + end @category.permission = CategoryGroup.permission_types[:full] if Category.topic_create_allowed(guardian).where(id: @category.id).exists? render_serialized(@category, CategorySerializer) diff --git a/app/controllers/composer_controller.rb b/app/controllers/composer_controller.rb index 0f54f282b7..7f275027e5 100644 --- a/app/controllers/composer_controller.rb +++ b/app/controllers/composer_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'html_to_markdown' - class ComposerController < ApplicationController requires_login diff --git a/app/controllers/composer_messages_controller.rb b/app/controllers/composer_messages_controller.rb index 97d91a4d3e..c5d4de3c64 100644 --- a/app/controllers/composer_messages_controller.rb +++ b/app/controllers/composer_messages_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'composer_messages_finder' - class ComposerMessagesController < ApplicationController requires_login diff --git a/app/controllers/embed_controller.rb b/app/controllers/embed_controller.rb index 5490fa59fa..0d8f53eed2 100644 --- a/app/controllers/embed_controller.rb +++ b/app/controllers/embed_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'topic_query_params' - class EmbedController < ApplicationController include TopicQueryParams diff --git a/app/controllers/exceptions_controller.rb b/app/controllers/exceptions_controller.rb index 225c383d6c..2cbfeeed02 100644 --- a/app/controllers/exceptions_controller.rb +++ b/app/controllers/exceptions_controller.rb @@ -2,7 +2,6 @@ class ExceptionsController < ApplicationController skip_before_action :check_xhr, :preload_json - before_action :hide_search def not_found # centralize all rendering of 404 into app controller @@ -11,13 +10,7 @@ class ExceptionsController < ApplicationController # Give us an endpoint to use for 404 content in the ember app def not_found_body - render html: build_not_found_page(200, false) - end - - private - - def hide_search - @hide_search = true if SiteSetting.login_required + render html: build_not_found_page(status: 200) end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 461a98074b..f0ed98e57d 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class GroupsController < ApplicationController - include ApplicationHelper - requires_login only: [ :set_notifications, :mentionable, @@ -46,7 +44,7 @@ class GroupsController < ApplicationController raise Discourse::InvalidAccess.new(:enable_group_directory) end - page_size = mobile_device? ? 15 : 36 + page_size = MobileDetection.mobile_device?(request.user_agent) ? 15 : 36 page = params[:page]&.to_i || 0 order = %w{name user_count}.delete(params[:order]) dir = params[:asc] ? 'ASC' : 'DESC' diff --git a/app/controllers/inline_onebox_controller.rb b/app/controllers/inline_onebox_controller.rb index 4fa1db159c..16e4cf267b 100644 --- a/app/controllers/inline_onebox_controller.rb +++ b/app/controllers/inline_onebox_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'inline_oneboxer' - class InlineOneboxController < ApplicationController requires_login diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index cef597971f..7a62d95a56 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'rate_limiter' - class InvitesController < ApplicationController requires_login only: [ @@ -170,6 +168,8 @@ class InvitesController < ApplicationController end def upload_csv + require 'csv' + guardian.ensure_can_bulk_invite_to_forum!(current_user) hijack do diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index 6d2bace341..312b721fb8 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require_dependency 'topic_list_responder' -require_dependency 'topic_query_params' - class ListController < ApplicationController include TopicListResponder include TopicQueryParams @@ -371,7 +368,13 @@ class ListController < ApplicationController raise Discourse::NotFound.new("category not found", check_permalinks: true) if !@category @description_meta = @category.description_text - raise Discourse::NotFound unless guardian.can_see?(@category) + if !guardian.can_see?(@category) + if SiteSetting.detailed_404 + raise Discourse::InvalidAccess + else + raise Discourse::NotFound + end + end if use_crawler_layout? @subcategories = @category.subcategories.select { |c| guardian.can_see?(c) } diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 2ec11b39c8..94779fc0a1 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'notification_serializer' - class NotificationsController < ApplicationController requires_login diff --git a/app/controllers/onebox_controller.rb b/app/controllers/onebox_controller.rb index 877504df34..10c500317b 100644 --- a/app/controllers/onebox_controller.rb +++ b/app/controllers/onebox_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'oneboxer' - class OneboxController < ApplicationController requires_login diff --git a/app/controllers/post_action_users_controller.rb b/app/controllers/post_action_users_controller.rb index 642c40dd4e..9cfbbc1b11 100644 --- a/app/controllers/post_action_users_controller.rb +++ b/app/controllers/post_action_users_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'discourse' - class PostActionUsersController < ApplicationController def index params.require(:post_action_type_id) diff --git a/app/controllers/post_actions_controller.rb b/app/controllers/post_actions_controller.rb index e652fb4ce2..594c7062ce 100644 --- a/app/controllers/post_actions_controller.rb +++ b/app/controllers/post_actions_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'discourse' - class PostActionsController < ApplicationController requires_login diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index d5a9a12c0d..b63d2539fe 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -1,14 +1,5 @@ # frozen_string_literal: true -require_dependency 'new_post_manager' -require_dependency 'post_creator' -require_dependency 'post_action_destroyer' -require_dependency 'post_destroyer' -require_dependency 'post_merger' -require_dependency 'distributed_memoizer' -require_dependency 'new_post_result_serializer' -require_dependency 'post_locker' - class PostsController < ApplicationController requires_login except: [ diff --git a/app/controllers/reviewables_controller.rb b/app/controllers/reviewables_controller.rb index dffe6cd172..2e9816bab1 100644 --- a/app/controllers/reviewables_controller.rb +++ b/app/controllers/reviewables_controller.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require_dependency 'reviewable_explanation_serializer' class ReviewablesController < ApplicationController requires_login diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 82d8ac74bc..919303e3be 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'search' - class SearchController < ApplicationController skip_before_action :check_xhr, only: :show diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index 08a3d9f249..16aa29c809 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -1,10 +1,5 @@ # frozen_string_literal: true -require_dependency 'rate_limiter' -require_dependency 'single_sign_on' -require_dependency 'single_sign_on_provider' -require_dependency 'url_helper' - class SessionController < ApplicationController class LocalLoginNotAllowed < StandardError; end rescue_from LocalLoginNotAllowed do @@ -298,7 +293,18 @@ class SessionController < ApplicationController if payload = login_error_check(user) render json: payload else - if user.totp_enabled? && !user.authenticate_second_factor(params[:second_factor_token], params[:second_factor_method].to_i) + if (params[:second_factor_token].blank?) + security_key_valid = ::Webauthn::SecurityKeyAuthenticationService.new(user, params[:security_key_credential], + challenge: secure_session["staged-webauthn-challenge-#{user.id}"], + rp_id: secure_session["staged-webauthn-rp-id-#{user.id}"], + origin: Discourse.base_url + ).authenticate_security_key + return invalid_security_key(user) if user.security_keys_enabled? && !security_key_valid + end + + if user.totp_enabled? && \ + !user.authenticate_second_factor(params[:second_factor_token], params[:second_factor_method].to_i) && + !params[:security_key_credential].present? return render json: failed_json.merge( error: I18n.t("login.invalid_second_factor_code"), reason: "invalid_second_factor", @@ -308,6 +314,17 @@ class SessionController < ApplicationController (user.active && user.email_confirmed?) ? login(user) : not_activated(user) end + rescue ::Webauthn::SecurityKeyError => err + invalid_security_key(user, err.message) + end + + def invalid_security_key(user, err_message = nil) + stage_webauthn_security_key_challenge(user) if !params[:security_key_credential] + return render json: failed_json.merge( + error: err_message || I18n.t("login.invalid_security_key"), + reason: "invalid_security_key", + backup_enabled: user.backup_codes_enabled? + ).merge(webauthn_security_key_challenge_and_allowed_credentials(user)) end def email_login_info @@ -323,10 +340,18 @@ class SessionController < ApplicationController token_email: matched_token.email } - if matched_token.user&.totp_enabled? + matched_user = matched_token.user + if matched_user&.totp_enabled? response.merge!( second_factor_required: true, - backup_codes_enabled: matched_token.user&.backup_codes_enabled? + backup_codes_enabled: matched_user&.backup_codes_enabled? + ) + end + + if matched_user&.security_keys_enabled? + stage_webauthn_security_key_challenge(matched_user) + response.merge!( + webauthn_security_key_challenge_and_allowed_credentials(matched_user).merge(security_key_required: true) ) end @@ -343,15 +368,27 @@ class SessionController < ApplicationController raise Discourse::NotFound if !SiteSetting.enable_local_logins_via_email second_factor_token = params[:second_factor_token] second_factor_method = params[:second_factor_method].to_i + security_key_credential = params[:security_key_credential] token = params[:token] matched_token = EmailToken.confirmable(token) - if matched_token&.user&.totp_enabled? - if !second_factor_token.present? - return render json: { error: I18n.t('login.invalid_second_factor_code') } - elsif !matched_token.user.authenticate_second_factor(second_factor_token, second_factor_method) - RateLimiter.new(nil, "second-factor-min-#{request.remote_ip}", 3, 1.minute).performed! - return render json: { error: I18n.t('login.invalid_second_factor_code') } + if security_key_credential.present? + if matched_token&.user&.security_keys_enabled? + security_key_valid = ::Webauthn::SecurityKeyAuthenticationService.new(matched_token&.user, params[:security_key_credential], + challenge: secure_session["staged-webauthn-challenge-#{matched_token&.user&.id}"], + rp_id: secure_session["staged-webauthn-rp-id-#{matched_token&.user&.id}"], + origin: Discourse.base_url + ).authenticate_security_key + return invalid_security_key(matched_token&.user) if !security_key_valid + end + else + if matched_token&.user&.totp_enabled? + if !second_factor_token.present? + return render json: { error: I18n.t('login.invalid_second_factor_code') } + elsif !matched_token.user.authenticate_second_factor(second_factor_token, second_factor_method) + RateLimiter.new(nil, "second-factor-min-#{request.remote_ip}", 3, 1.minute).performed! + return render json: { error: I18n.t('login.invalid_second_factor_code') } + end end end @@ -367,6 +404,8 @@ class SessionController < ApplicationController end return render json: { error: I18n.t('email_login.invalid_token') } + rescue ::Webauthn::SecurityKeyError => err + invalid_security_key(user, err.message) end def one_time_password @@ -535,4 +574,21 @@ class SessionController < ApplicationController def sso_url(sso) sso.to_url end + + def stage_webauthn_security_key_challenge(user) + challenge = SecureRandom.hex(30) + secure_session["staged-webauthn-challenge-#{user.id}"] = challenge + secure_session["staged-webauthn-rp-id-#{user.id}"] = Discourse.current_hostname + end + + def webauthn_security_key_challenge_and_allowed_credentials(user) + return {} if !user.security_keys_enabled? + credential_ids = user.security_keys.select(:credential_id) + .where(factor_type: UserSecurityKey.factor_types[:second_factor]) + .pluck(:credential_id) + { + allowed_credential_ids: credential_ids, + challenge: secure_session["staged-webauthn-challenge-#{user.id}"] + } + end end diff --git a/app/controllers/similar_topics_controller.rb b/app/controllers/similar_topics_controller.rb index b2a5809505..a42677893d 100644 --- a/app/controllers/similar_topics_controller.rb +++ b/app/controllers/similar_topics_controller.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require_dependency 'similar_topic_serializer' -require_dependency 'search/grouped_search_results' - class SimilarTopicsController < ApplicationController class SimilarTopic diff --git a/app/controllers/site_controller.rb b/app/controllers/site_controller.rb index 8c1ed8d2de..f1ea98d34d 100644 --- a/app/controllers/site_controller.rb +++ b/app/controllers/site_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'site_serializer' - class SiteController < ApplicationController layout false skip_before_action :preload_json, :check_xhr diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb index b546c2bc86..51b291403c 100644 --- a/app/controllers/static_controller.rb +++ b/app/controllers/static_controller.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require_dependency 'distributed_memoizer' -require_dependency 'file_helper' - class StaticController < ApplicationController skip_before_action :check_xhr, :redirect_to_login_if_required diff --git a/app/controllers/steps_controller.rb b/app/controllers/steps_controller.rb index 5569841838..1f17ae6af9 100644 --- a/app/controllers/steps_controller.rb +++ b/app/controllers/steps_controller.rb @@ -1,9 +1,5 @@ # frozen_string_literal: true -require_dependency 'wizard' -require_dependency 'wizard/builder' -require_dependency 'wizard/step_updater' - class StepsController < ApplicationController requires_login diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 7e06d03d6b..91439d66d1 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -1,10 +1,5 @@ # frozen_string_literal: true -require_dependency 'topic_list_responder' -require_dependency 'topic_query_params' -require_dependency 'topics_bulk_action' -require_dependency 'topic_query' - class TagsController < ::ApplicationController include TopicListResponder include TopicQueryParams @@ -125,6 +120,8 @@ class TagsController < ::ApplicationController end def upload + require 'csv' + guardian.ensure_can_admin_tags! file = params[:file] || params[:files].first diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 087d00fc10..9de9c79592 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -1,14 +1,5 @@ # frozen_string_literal: true -require_dependency 'topic_view' -require_dependency 'promotion' -require_dependency 'url_helper' -require_dependency 'topics_bulk_action' -require_dependency 'discourse_event' -require_dependency 'rate_limiter' -require_dependency 'topic_publisher' -require_dependency 'post_action_destroyer' - class TopicsController < ApplicationController requires_login only: [ :timings, @@ -88,12 +79,54 @@ class TopicsController < ApplicationController begin @topic_view = TopicView.new(params[:id] || params[:topic_id], current_user, opts) - rescue Discourse::NotFound + rescue Discourse::NotFound => ex if params[:id] topic = Topic.find_by(slug: params[:id].downcase) return redirect_to_correct_topic(topic, opts[:post_number]) if topic end - raise Discourse::NotFound + + raise ex + rescue Discourse::NotLoggedIn => ex + if !SiteSetting.detailed_404 + raise Discourse::NotFound + else + raise ex + end + rescue Discourse::InvalidAccess => ex + # If the user can't see the topic, clean up notifications for it. + Notification.remove_for(current_user.id, params[:topic_id]) if current_user + + deleted = guardian.can_see_topic?(ex.obj, false) || + (!guardian.can_see_topic?(ex.obj) && + ex.obj&.access_topic_via_group && + ex.obj.deleted_at) + + if SiteSetting.detailed_404 + if deleted + raise Discourse::NotFound.new( + 'deleted topic', + custom_message: 'deleted_topic', + status: 410, + check_permalinks: true, + original_path: ex.obj.relative_url + ) + elsif !guardian.can_see_topic?(ex.obj) && group = ex.obj&.access_topic_via_group + raise Discourse::InvalidAccess.new( + 'not in group', + ex.obj, + custom_message: 'not_in_group.title_topic', + group: group + ) + end + + raise ex + else + raise Discourse::NotFound.new( + nil, + check_permalinks: deleted, + original_path: ex.obj.relative_url + ) + end end page = params[:page] @@ -129,27 +162,6 @@ class TopicsController < ApplicationController end perform_show_response - - rescue Discourse::InvalidAccess => ex - if !guardian.can_see_topic?(ex.obj) && guardian.can_get_access_to_topic?(ex.obj) - return perform_hidden_topic_show_response(ex.obj) - end - - if current_user - # If the user can't see the topic, clean up notifications for it. - Notification.remove_for(current_user.id, params[:topic_id]) - end - - if ex.obj && Topic === ex.obj && guardian.can_see_topic_if_not_deleted?(ex.obj) - raise Discourse::NotFound.new( - "topic was deleted", - status: 410, - check_permalinks: true, - original_path: ex.obj.relative_url - ) - end - - raise ex end def publish @@ -969,19 +981,6 @@ class TopicsController < ApplicationController end end - def perform_hidden_topic_show_response(topic) - respond_to do |format| - format.html do - @topic_view = nil - render :show - end - - format.json do - render_serialized(topic, HiddenTopicViewSerializer, root: false) - end - end - end - def render_topic_changes(dest_topic) if dest_topic.present? render json: { success: true, url: dest_topic.relative_url } diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 2fe9c0c9c9..78c1623704 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true require "mini_mime" -require_dependency 'upload_creator' -require_dependency "file_store/local_store" class UploadsController < ApplicationController requires_login except: [:show, :show_short] diff --git a/app/controllers/user_avatars_controller.rb b/app/controllers/user_avatars_controller.rb index 11f4f25cf5..288fb8bc3f 100644 --- a/app/controllers/user_avatars_controller.rb +++ b/app/controllers/user_avatars_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'letter_avatar' - class UserAvatarsController < ApplicationController skip_before_action :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show, :show_letter, :show_proxy_letter] diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index f1f43f1f85..7e7e108a35 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -1,10 +1,6 @@ # -*- encoding : utf-8 -*- # frozen_string_literal: true -require_dependency 'email' -require_dependency 'enum' -require_dependency 'user_name_suggester' - class Users::OmniauthCallbacksController < ApplicationController skip_before_action :redirect_to_login_if_required @@ -75,18 +71,9 @@ class Users::OmniauthCallbacksController < ApplicationController else @auth_result.authenticator_name = authenticator.name complete_response_data - - if provider&.full_screen_login || cookies['fsl'] - cookies.delete('fsl') - cookies['_bypass_cache'] = true - cookies[:authentication_data] = @auth_result.to_client_hash.to_json - redirect_to @origin - else - respond_to do |format| - format.html - format.json { render json: @auth_result.to_client_hash } - end - end + cookies['_bypass_cache'] = true + cookies[:authentication_data] = @auth_result.to_client_hash.to_json + redirect_to @origin end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 7a6225a15a..629faaa33c 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,12 +1,5 @@ # frozen_string_literal: true -require_dependency 'discourse_hub' -require_dependency 'user_name_suggester' -require_dependency 'rate_limiter' -require_dependency 'wizard' -require_dependency 'wizard/builder' -require_dependency 'admin_confirmation' - class UsersController < ApplicationController skip_before_action :authorize_mini_profiler, only: [:avatar] @@ -17,7 +10,8 @@ class UsersController < ApplicationController :topic_tracking_state, :preferences, :create_second_factor_totp, :enable_second_factor_totp, :disable_second_factor, :list_second_factors, :update_second_factor, :create_second_factor_backup, :select_avatar, - :notification_level, :revoke_auth_token + :notification_level, :revoke_auth_token, :register_second_factor_security_key, + :create_second_factor_security_key ] skip_before_action :check_xhr, only: [ @@ -28,7 +22,9 @@ class UsersController < ApplicationController before_action :second_factor_check_confirmed_password, only: [ :create_second_factor_totp, :enable_second_factor_totp, - :disable_second_factor, :update_second_factor, :create_second_factor_backup] + :disable_second_factor, :update_second_factor, :create_second_factor_backup, + :register_second_factor_security_key, :create_second_factor_security_key + ] before_action :respond_to_suspicious_request, only: [:create] @@ -496,17 +492,33 @@ class UsersController < ApplicationController second_factor_token = params[:second_factor_token] second_factor_method = params[:second_factor_method].to_i + security_key_credential = params[:security_key_credential] if second_factor_token.present? && UserSecondFactor.methods[second_factor_method] RateLimiter.new(nil, "second-factor-min-#{request.remote_ip}", 3, 1.minute).performed! second_factor_authenticated = @user&.authenticate_second_factor(second_factor_token, second_factor_method) + elsif security_key_credential.present? + security_key_authenticated = ::Webauthn::SecurityKeyAuthenticationService.new( + @user, + security_key_credential, + challenge: secure_session["staged-webauthn-challenge-#{@user.id}"], + rp_id: secure_session["staged-webauthn-rp-id-#{@user.id}"], + origin: Discourse.base_url + ).authenticate_security_key end - if second_factor_authenticated || !@user&.totp_enabled? + second_factor_totp_disabled = !@user&.totp_enabled? + if second_factor_authenticated || second_factor_totp_disabled || security_key_authenticated secure_session["second-factor-#{token}"] = "true" end + security_key_disabled = !@user&.security_keys_enabled? + if security_key_authenticated || security_key_disabled + secure_session["security-key-#{token}"] = "true" + end + valid_second_factor = secure_session["second-factor-#{token}"] == "true" + valid_security_key = secure_session["security-key-#{token}"] == "true" if !@user @error = I18n.t('password_reset.no_token') @@ -539,13 +551,17 @@ class UsersController < ApplicationController if @error render layout: 'no_ember' else + stage_webauthn_security_key_challenge(@user) store_preloaded( "password_reset", MultiJson.dump( - is_developer: UsernameCheckerService.is_developer?(@user.email), - admin: @user.admin?, - second_factor_required: !valid_second_factor, - backup_enabled: @user.backup_codes_enabled? + { + is_developer: UsernameCheckerService.is_developer?(@user.email), + admin: @user.admin?, + second_factor_required: !valid_second_factor, + security_key_required: !valid_security_key, + backup_enabled: @user.backup_codes_enabled? + }.merge(webauthn_security_key_challenge_and_allowed_credentials(@user)) ) ) end @@ -578,18 +594,23 @@ class UsersController < ApplicationController errors: @user&.errors&.to_hash } else + stage_webauthn_security_key_challenge(@user) if !valid_security_key && !security_key_credential.present? render json: { is_developer: UsernameCheckerService.is_developer?(@user.email), admin: @user.admin?, second_factor_required: !valid_second_factor, + security_key_required: !valid_security_key, backup_enabled: @user.backup_codes_enabled? - } + }.merge(webauthn_security_key_challenge_and_allowed_credentials(@user)) end end end end - rescue RateLimiter::LimitExceeded => e - render_rate_limit_error(e) + rescue ::Webauthn::SecurityKeyError => err + render json: { + message: err.message, + errors: [err.message] + } end def confirm_email_token @@ -636,27 +657,55 @@ class UsersController < ApplicationController email_token_user = EmailToken.confirmable(token)&.user totp_enabled = email_token_user&.totp_enabled? + security_keys_enabled = email_token_user&.security_keys_enabled? second_factor_token = params[:second_factor_token] second_factor_method = params[:second_factor_method].to_i + security_key_credential = params[:security_key_credential] confirm_email = false + @security_key_required = security_keys_enabled - confirm_email = + if security_keys_enabled && params[:security_key_credential].blank? + stage_webauthn_security_key_challenge(email_token_user) + challenge_and_credentials = webauthn_security_key_challenge_and_allowed_credentials(email_token_user) + @security_key_challenge = challenge_and_credentials[:challenge] + @security_key_allowed_credential_ids = challenge_and_credentials[:allowed_credential_ids].join(",") + end + + if security_keys_enabled && params[:security_key_credential].present? + credential = JSON.parse(params[:security_key_credential]).with_indifferent_access + + confirm_email = ::Webauthn::SecurityKeyAuthenticationService.new(email_token_user, credential, + challenge: secure_session["staged-webauthn-challenge-#{email_token_user&.id}"], + rp_id: secure_session["staged-webauthn-rp-id-#{email_token_user&.id}"], + origin: Discourse.base_url + ).authenticate_security_key + @message = I18n.t('login.security_key_invalid') if !confirm_email + elsif security_keys_enabled && second_factor_token.blank? + confirm_email = false + @message = I18n.t("login.second_factor_title") if totp_enabled @second_factor_required = true @backup_codes_enabled = true - @message = I18n.t("login.second_factor_title") - - if second_factor_token.present? - if email_token_user.authenticate_second_factor(second_factor_token, second_factor_method) - true - else - @error = I18n.t("login.invalid_second_factor_code") - false - end - end - else - true end + else + confirm_email = + if totp_enabled + @second_factor_required = true + @backup_codes_enabled = true + @message = I18n.t("login.second_factor_title") + + if second_factor_token.present? + if email_token_user.authenticate_second_factor(second_factor_token, second_factor_method) + true + else + @error = I18n.t("login.invalid_second_factor_code") + false + end + end + else + true + end + end if confirm_email @user = EmailToken.confirm(token) @@ -673,10 +722,13 @@ class UsersController < ApplicationController end end - render layout: false + render layout: 'no_ember' rescue RateLimiter::LimitExceeded @message = I18n.t("rate_limiter.slow_down") - render layout: false + render layout: 'no_ember' + rescue ::Webauthn::SecurityKeyError => err + @message = err.message + render layout: 'no_ember' end def email_login @@ -1110,8 +1162,15 @@ class UsersController < ApplicationController end if secure_session["confirmed-password-#{current_user.id}"] == "true" + totp_second_factors = current_user.totps + .select(:id, :name, :last_used, :created_at, :method) + .where(enabled: true).order(:created_at) + + security_keys = current_user.security_keys.where(factor_type: UserSecurityKey.factor_types[:second_factor]).order(:created_at) + render json: success_json.merge( - totps: current_user.totps.select(:id, :name, :last_used, :created_at, :method).order(:created_at) + totps: totp_second_factors, + security_keys: security_keys ) else render json: success_json.merge( @@ -1144,6 +1203,55 @@ class UsersController < ApplicationController ) end + def create_second_factor_security_key + challenge = SecureRandom.hex(30) + secure_session["staged-webauthn-challenge-#{current_user.id}"] = challenge + secure_session["staged-webauthn-rp-id-#{current_user.id}"] = Discourse.current_hostname + secure_session["staged-webauthn-rp-name-#{current_user.id}"] = SiteSetting.title + + render json: success_json.merge( + challenge: challenge, + rp_id: Discourse.current_hostname, + rp_name: SiteSetting.title, + supported_algoriths: ::Webauthn::SUPPORTED_ALGORITHMS, + user_secure_id: current_user.create_or_fetch_secure_identifier, + existing_active_credential_ids: current_user.second_factor_security_key_credential_ids + ) + end + + def register_second_factor_security_key + params.require(:name) + params.require(:attestation) + params.require(:clientData) + + ::Webauthn::SecurityKeyRegistrationService.new( + current_user, + params, + challenge: secure_session["staged-webauthn-challenge-#{current_user.id}"], + rp_id: secure_session["staged-webauthn-rp-id-#{current_user.id}"], + origin: Discourse.base_url + ).register_second_factor_security_key + render json: success_json + rescue ::Webauthn::SecurityKeyError => err + render json: failed_json.merge( + error: err.message + ) + end + + def update_security_key + user_security_key = current_user.security_keys.find_by(id: params[:id].to_i) + raise Discourse::InvalidParameters unless user_security_key + + if params[:name] && !params[:name].blank? + user_security_key.update!(name: params[:name]) + end + if params[:disable] == "true" + user_security_key.update!(enabled: false) + end + + render json: success_json + end + def enable_second_factor_totp params.require(:second_factor_token) params.require(:name) @@ -1373,4 +1481,19 @@ class UsersController < ApplicationController end end end + + def stage_webauthn_security_key_challenge(user) + challenge = SecureRandom.hex(30) + secure_session["staged-webauthn-challenge-#{user.id}"] = challenge + secure_session["staged-webauthn-rp-id-#{user.id}"] = Discourse.current_hostname + end + + def webauthn_security_key_challenge_and_allowed_credentials(user) + return {} if !user.security_keys_enabled? + credential_ids = user.second_factor_security_key_credential_ids + { + allowed_credential_ids: credential_ids, + challenge: secure_session["staged-webauthn-challenge-#{user.id}"] + } + end end diff --git a/app/controllers/users_email_controller.rb b/app/controllers/users_email_controller.rb index 53d4cc2871..f74f60a94a 100644 --- a/app/controllers/users_email_controller.rb +++ b/app/controllers/users_email_controller.rb @@ -1,9 +1,5 @@ # frozen_string_literal: true -require_dependency 'rate_limiter' -require_dependency 'email_validator' -require_dependency 'email_updater' - class UsersEmailController < ApplicationController requires_login only: [:index, :update] diff --git a/app/controllers/wizard_controller.rb b/app/controllers/wizard_controller.rb index 77c44055cc..6b573bdace 100644 --- a/app/controllers/wizard_controller.rb +++ b/app/controllers/wizard_controller.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require_dependency 'wizard' -require_dependency 'wizard/builder' - class WizardController < ApplicationController requires_login except: [:qunit] diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3995c88dbf..84d8e383b5 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -2,14 +2,6 @@ # frozen_string_literal: true require 'current_user' require 'canonical_url' -require_dependency 'guardian' -require_dependency 'unread' -require_dependency 'age_words' -require_dependency 'configurable_urls' -require_dependency 'mobile_detection' -require_dependency 'category_badge' -require_dependency 'global_path' -require_dependency 'emoji' module ApplicationHelper include CurrentUser diff --git a/app/jobs/base.rb b/app/jobs/base.rb index aae9c8a107..9ad78f4564 100644 --- a/app/jobs/base.rb +++ b/app/jobs/base.rb @@ -187,7 +187,7 @@ module Jobs def perform(*args) opts = args.extract_options!.with_indifferent_access - if Jobs.run_later? + if ::Jobs.run_later? Sidekiq.redis do |r| r.set('last_job_perform_at', Time.now.to_i) end @@ -275,7 +275,7 @@ module Jobs extend MiniScheduler::Schedule def perform(*args) - if (Jobs::Heartbeat === self) || !Discourse.readonly_mode? + if (::Jobs::Heartbeat === self) || !Discourse.readonly_mode? super end end @@ -290,7 +290,8 @@ module Jobs end # If we are able to queue a job, do it - if Jobs.run_later? + + if ::Jobs.run_later? hash = { 'class' => klass, 'args' => [opts] @@ -361,7 +362,3 @@ module Jobs end end end - -Dir["#{Rails.root}/app/jobs/onceoff/*.rb"].each { |file| require_dependency file } -Dir["#{Rails.root}/app/jobs/regular/*.rb"].each { |file| require_dependency file } -Dir["#{Rails.root}/app/jobs/scheduled/*.rb"].each { |file| require_dependency file } diff --git a/app/jobs/onceoff/clean_up_post_timings.rb b/app/jobs/onceoff/clean_up_post_timings.rb index 43c0e5caba..1399ddaa80 100644 --- a/app/jobs/onceoff/clean_up_post_timings.rb +++ b/app/jobs/onceoff/clean_up_post_timings.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class CleanUpPostTimings < Jobs::Onceoff + class CleanUpPostTimings < ::Jobs::Onceoff # Remove post timings that are remnants of previous post moves # or other shenanigans and don't reference a valid user or post anymore. diff --git a/app/jobs/onceoff/clean_up_sidekiq_statistic.rb b/app/jobs/onceoff/clean_up_sidekiq_statistic.rb index c99a87656d..397fc4f79b 100644 --- a/app/jobs/onceoff/clean_up_sidekiq_statistic.rb +++ b/app/jobs/onceoff/clean_up_sidekiq_statistic.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class CleanUpSidekiqStatistic < Jobs::Onceoff + class CleanUpSidekiqStatistic < ::Jobs::Onceoff def execute_onceoff(args) $redis.without_namespace.del('sidekiq:sidekiq:statistic') end diff --git a/app/jobs/onceoff/clean_up_user_export_topics.rb b/app/jobs/onceoff/clean_up_user_export_topics.rb index 9a5b19e05a..8e5be5bf27 100644 --- a/app/jobs/onceoff/clean_up_user_export_topics.rb +++ b/app/jobs/onceoff/clean_up_user_export_topics.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class CleanUpUserExportTopics < Jobs::Onceoff + class CleanUpUserExportTopics < ::Jobs::Onceoff def execute_onceoff(args) translated_keys = I18n.available_locales.map do |l| I18n.with_locale(:"#{l}") { I18n.t("system_messages.csv_export_succeeded.subject_template") } diff --git a/app/jobs/onceoff/clear_width_and_height.rb b/app/jobs/onceoff/clear_width_and_height.rb index 070c16056a..7c82840740 100644 --- a/app/jobs/onceoff/clear_width_and_height.rb +++ b/app/jobs/onceoff/clear_width_and_height.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class ClearWidthAndHeight < Jobs::Onceoff + class ClearWidthAndHeight < ::Jobs::Onceoff def execute_onceoff(args) # we have to clear all old uploads cause # we could have old versions of height / width diff --git a/app/jobs/onceoff/correct_missing_dualstack_urls.rb b/app/jobs/onceoff/correct_missing_dualstack_urls.rb index a8de7b4c69..6cdfa55e84 100644 --- a/app/jobs/onceoff/correct_missing_dualstack_urls.rb +++ b/app/jobs/onceoff/correct_missing_dualstack_urls.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class CorrectMissingDualstackUrls < Jobs::Onceoff + class CorrectMissingDualstackUrls < ::Jobs::Onceoff def execute_onceoff(args) # s3 now uses dualstack urls, keep them around correctly # in both uploads and optimized_image tables diff --git a/app/jobs/onceoff/create_tags_search_index.rb b/app/jobs/onceoff/create_tags_search_index.rb index f683834e65..f761707426 100644 --- a/app/jobs/onceoff/create_tags_search_index.rb +++ b/app/jobs/onceoff/create_tags_search_index.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class CreateTagsSearchIndex < Jobs::Onceoff + class CreateTagsSearchIndex < ::Jobs::Onceoff def execute_onceoff(args) DB.query('select id, name from tags').each do |t| SearchIndexer.update_tags_index(t.id, t.name) diff --git a/app/jobs/onceoff/fix_featured_link_for_topics.rb b/app/jobs/onceoff/fix_featured_link_for_topics.rb index 4d3c48c913..337d7d2efa 100644 --- a/app/jobs/onceoff/fix_featured_link_for_topics.rb +++ b/app/jobs/onceoff/fix_featured_link_for_topics.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class FixFeaturedLinkForTopics < Jobs::Onceoff + class FixFeaturedLinkForTopics < ::Jobs::Onceoff def execute_onceoff(args) Topic.where("featured_link IS NOT NULL").find_each do |topic| featured_link = topic.featured_link diff --git a/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb b/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb index 80fc0f1d27..bbbc942603 100644 --- a/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb +++ b/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class FixInvalidGravatarUploads < Jobs::Onceoff + class FixInvalidGravatarUploads < ::Jobs::Onceoff def execute_onceoff(args) Upload.where(original_filename: "gravatar.png").find_each do |upload| # note, this still feels pretty expensive for a once off diff --git a/app/jobs/onceoff/fix_invalid_upload_extensions.rb b/app/jobs/onceoff/fix_invalid_upload_extensions.rb index 8c62bdbfc1..a5a6f98921 100644 --- a/app/jobs/onceoff/fix_invalid_upload_extensions.rb +++ b/app/jobs/onceoff/fix_invalid_upload_extensions.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true -require_dependency "upload_fixer" - module Jobs - class FixInvalidUploadExtensions < Jobs::Onceoff + class FixInvalidUploadExtensions < ::Jobs::Onceoff def execute_onceoff(args) UploadFixer.fix_all_extensions end diff --git a/app/jobs/onceoff/fix_out_of_sync_user_uploaded_avatar.rb b/app/jobs/onceoff/fix_out_of_sync_user_uploaded_avatar.rb index 1029edc916..2a167697c2 100644 --- a/app/jobs/onceoff/fix_out_of_sync_user_uploaded_avatar.rb +++ b/app/jobs/onceoff/fix_out_of_sync_user_uploaded_avatar.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class FixOutOfSyncUserUploadedAvatar < Jobs::Onceoff + class FixOutOfSyncUserUploadedAvatar < ::Jobs::Onceoff def execute_onceoff(args) DB.exec(<<~SQL) WITH X AS ( diff --git a/app/jobs/onceoff/fix_posts_read.rb b/app/jobs/onceoff/fix_posts_read.rb index 206b5bc41f..9d4787065a 100644 --- a/app/jobs/onceoff/fix_posts_read.rb +++ b/app/jobs/onceoff/fix_posts_read.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class FixPostsRead < Jobs::Onceoff + class FixPostsRead < ::Jobs::Onceoff def execute_onceoff(args) # Skipping to the last post in a topic used to count all posts in the topic # as read in user stats. Cap the posts read count to 50 * topics_entered. diff --git a/app/jobs/onceoff/fix_primary_emails_for_staged_users.rb b/app/jobs/onceoff/fix_primary_emails_for_staged_users.rb index a60b1d6315..5fb9ff218e 100644 --- a/app/jobs/onceoff/fix_primary_emails_for_staged_users.rb +++ b/app/jobs/onceoff/fix_primary_emails_for_staged_users.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true -require_dependency 'user_destroyer' - module Jobs - class FixPrimaryEmailsForStagedUsers < Jobs::Onceoff + class FixPrimaryEmailsForStagedUsers < ::Jobs::Onceoff def execute_onceoff(args) users = User.where(active: false, staged: true).joins(:email_tokens) acting_user = Discourse.system_user diff --git a/app/jobs/onceoff/fix_retro_anniversary.rb b/app/jobs/onceoff/fix_retro_anniversary.rb index bec4d8d62f..a6ab1c566f 100644 --- a/app/jobs/onceoff/fix_retro_anniversary.rb +++ b/app/jobs/onceoff/fix_retro_anniversary.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true -require_dependency 'jobs/scheduled/grant_anniversary_badges' - module Jobs - class FixRetroAnniversary < Jobs::Onceoff + class FixRetroAnniversary < ::Jobs::Onceoff def execute_onceoff(args) return unless SiteSetting.enable_badges diff --git a/app/jobs/onceoff/fix_s3_etags.rb b/app/jobs/onceoff/fix_s3_etags.rb index 13cc3b0670..226b09e937 100644 --- a/app/jobs/onceoff/fix_s3_etags.rb +++ b/app/jobs/onceoff/fix_s3_etags.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class FixS3Etags < Jobs::Onceoff + class FixS3Etags < ::Jobs::Onceoff def execute_onceoff(args) [Upload, OptimizedImage].each do |model| diff --git a/app/jobs/onceoff/grant_emoji.rb b/app/jobs/onceoff/grant_emoji.rb index 1b3b5d6a75..5f85b431b8 100644 --- a/app/jobs/onceoff/grant_emoji.rb +++ b/app/jobs/onceoff/grant_emoji.rb @@ -2,7 +2,7 @@ module Jobs - class GrantEmoji < Jobs::Onceoff + class GrantEmoji < ::Jobs::Onceoff def execute_onceoff(args) return unless SiteSetting.enable_badges to_award = {} diff --git a/app/jobs/onceoff/grant_first_reply_by_email.rb b/app/jobs/onceoff/grant_first_reply_by_email.rb index 7a9eb93361..c0287205d7 100644 --- a/app/jobs/onceoff/grant_first_reply_by_email.rb +++ b/app/jobs/onceoff/grant_first_reply_by_email.rb @@ -2,7 +2,7 @@ module Jobs - class GrantFirstReplyByEmail < Jobs::Onceoff + class GrantFirstReplyByEmail < ::Jobs::Onceoff def execute_onceoff(args) return unless SiteSetting.enable_badges diff --git a/app/jobs/onceoff/grant_onebox.rb b/app/jobs/onceoff/grant_onebox.rb index ad01a2832b..59cf443f4e 100644 --- a/app/jobs/onceoff/grant_onebox.rb +++ b/app/jobs/onceoff/grant_onebox.rb @@ -2,7 +2,7 @@ module Jobs - class GrantOnebox < Jobs::Onceoff + class GrantOnebox < ::Jobs::Onceoff sidekiq_options queue: 'low' def execute_onceoff(args) diff --git a/app/jobs/onceoff/init_category_tag_stats.rb b/app/jobs/onceoff/init_category_tag_stats.rb index a296880f33..2189f47471 100644 --- a/app/jobs/onceoff/init_category_tag_stats.rb +++ b/app/jobs/onceoff/init_category_tag_stats.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class InitCategoryTagStats < Jobs::Onceoff + class InitCategoryTagStats < ::Jobs::Onceoff def execute_onceoff(args) DB.exec "DELETE FROM category_tag_stats" diff --git a/app/jobs/onceoff/migrate_censored_words.rb b/app/jobs/onceoff/migrate_censored_words.rb index 25d49a1b7b..5de876107b 100644 --- a/app/jobs/onceoff/migrate_censored_words.rb +++ b/app/jobs/onceoff/migrate_censored_words.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class MigrateCensoredWords < Jobs::Onceoff + class MigrateCensoredWords < ::Jobs::Onceoff def execute_onceoff(args) row = DB.query_single("SELECT value FROM site_settings WHERE name = 'censored_words'") if row.count > 0 diff --git a/app/jobs/onceoff/migrate_custom_emojis.rb b/app/jobs/onceoff/migrate_custom_emojis.rb index 8986aeb896..866ec4e421 100644 --- a/app/jobs/onceoff/migrate_custom_emojis.rb +++ b/app/jobs/onceoff/migrate_custom_emojis.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true -require_dependency 'upload_creator' - module Jobs - class MigrateCustomEmojis < Jobs::Onceoff + class MigrateCustomEmojis < ::Jobs::Onceoff def execute_onceoff(args) return if Rails.env.test? diff --git a/app/jobs/onceoff/migrate_featured_links.rb b/app/jobs/onceoff/migrate_featured_links.rb index e8da50f94d..45f4c11def 100644 --- a/app/jobs/onceoff/migrate_featured_links.rb +++ b/app/jobs/onceoff/migrate_featured_links.rb @@ -2,7 +2,7 @@ module Jobs - class MigrateFeaturedLinks < Jobs::Onceoff + class MigrateFeaturedLinks < ::Jobs::Onceoff def execute_onceoff(args) TopicCustomField.where(name: "featured_link").find_each do |tcf| diff --git a/app/jobs/onceoff/migrate_tagging_plugin.rb b/app/jobs/onceoff/migrate_tagging_plugin.rb index a9d2f71040..d92d5692f4 100644 --- a/app/jobs/onceoff/migrate_tagging_plugin.rb +++ b/app/jobs/onceoff/migrate_tagging_plugin.rb @@ -2,7 +2,7 @@ module Jobs - class MigrateTaggingPlugin < Jobs::Onceoff + class MigrateTaggingPlugin < ::Jobs::Onceoff def execute_onceoff(args) all_tags = TopicCustomField.where(name: "tags").select('DISTINCT value').all.map(&:value) diff --git a/app/jobs/onceoff/migrate_upload_extensions.rb b/app/jobs/onceoff/migrate_upload_extensions.rb index e5d3366031..846944e285 100644 --- a/app/jobs/onceoff/migrate_upload_extensions.rb +++ b/app/jobs/onceoff/migrate_upload_extensions.rb @@ -2,7 +2,7 @@ module Jobs - class MigrateUploadExtensions < Jobs::Onceoff + class MigrateUploadExtensions < ::Jobs::Onceoff def execute_onceoff(args) Upload.find_each do |upload| upload.extension = File.extname(upload.original_filename)[1..10] diff --git a/app/jobs/onceoff/migrate_url_site_settings.rb b/app/jobs/onceoff/migrate_url_site_settings.rb index c2641b7e1f..89ca6e67b7 100644 --- a/app/jobs/onceoff/migrate_url_site_settings.rb +++ b/app/jobs/onceoff/migrate_url_site_settings.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class MigrateUrlSiteSettings < Jobs::Onceoff + class MigrateUrlSiteSettings < ::Jobs::Onceoff SETTINGS = [ ['logo_url', 'logo'], ['logo_small_url', 'logo_small'], diff --git a/app/jobs/onceoff.rb b/app/jobs/onceoff/onceoff.rb similarity index 93% rename from app/jobs/onceoff.rb rename to app/jobs/onceoff/onceoff.rb index def68e4e64..4de373399d 100644 --- a/app/jobs/onceoff.rb +++ b/app/jobs/onceoff/onceoff.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true -class Jobs::Onceoff < Jobs::Base +require_relative '../base.rb' + +class Jobs::Onceoff < ::Jobs::Base sidekiq_options retry: false def self.name_for(klass) diff --git a/app/jobs/onceoff/post_uploads_recovery.rb b/app/jobs/onceoff/post_uploads_recovery.rb index 4d92e402bc..5f942209eb 100644 --- a/app/jobs/onceoff/post_uploads_recovery.rb +++ b/app/jobs/onceoff/post_uploads_recovery.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true -require_dependency "upload_recovery" - module Jobs - class PostUploadsRecovery < Jobs::Onceoff + class PostUploadsRecovery < ::Jobs::Onceoff MIN_PERIOD = 30 MAX_PERIOD = 120 diff --git a/app/jobs/onceoff/remove_old_auto_close_jobs.rb b/app/jobs/onceoff/remove_old_auto_close_jobs.rb index 90af80415d..c7015b480d 100644 --- a/app/jobs/onceoff/remove_old_auto_close_jobs.rb +++ b/app/jobs/onceoff/remove_old_auto_close_jobs.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class RemoveOldAutoCloseJobs < Jobs::Onceoff + class RemoveOldAutoCloseJobs < ::Jobs::Onceoff def execute_onceoff(args) Jobs.cancel_scheduled_job(:close_topic) diff --git a/app/jobs/onceoff/retro_grant_anniversary.rb b/app/jobs/onceoff/retro_grant_anniversary.rb index a4c9c98b2a..dd73fe215f 100644 --- a/app/jobs/onceoff/retro_grant_anniversary.rb +++ b/app/jobs/onceoff/retro_grant_anniversary.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true -require_dependency 'jobs/scheduled/grant_anniversary_badges' - module Jobs - class RetroGrantAnniversary < Jobs::Onceoff + class RetroGrantAnniversary < ::Jobs::Onceoff def execute_onceoff(args) return unless SiteSetting.enable_badges diff --git a/app/jobs/onceoff/retro_recent_time_read.rb b/app/jobs/onceoff/retro_recent_time_read.rb index 0cf43669a3..0750103adf 100644 --- a/app/jobs/onceoff/retro_recent_time_read.rb +++ b/app/jobs/onceoff/retro_recent_time_read.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class RetroRecentTimeRead < Jobs::Onceoff + class RetroRecentTimeRead < ::Jobs::Onceoff def execute_onceoff(args) # update past records by evenly distributing total time reading among each post read sql = <<~SQL diff --git a/app/jobs/regular/admin_confirmation_email.rb b/app/jobs/regular/admin_confirmation_email.rb index d525ea62c3..23fd5b3d06 100644 --- a/app/jobs/regular/admin_confirmation_email.rb +++ b/app/jobs/regular/admin_confirmation_email.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true -require_dependency 'email/sender' - module Jobs - class AdminConfirmationEmail < Jobs::Base + class AdminConfirmationEmail < ::Jobs::Base sidekiq_options queue: 'critical' def execute(args) diff --git a/app/jobs/regular/anonymize_user.rb b/app/jobs/regular/anonymize_user.rb index 90c855fd3f..8dca0ca403 100644 --- a/app/jobs/regular/anonymize_user.rb +++ b/app/jobs/regular/anonymize_user.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class AnonymizeUser < Jobs::Base + class AnonymizeUser < ::Jobs::Base sidekiq_options queue: 'low' diff --git a/app/jobs/regular/automatic_group_membership.rb b/app/jobs/regular/automatic_group_membership.rb index 8695737ab2..e10bc7a013 100644 --- a/app/jobs/regular/automatic_group_membership.rb +++ b/app/jobs/regular/automatic_group_membership.rb @@ -2,7 +2,7 @@ module Jobs - class AutomaticGroupMembership < Jobs::Base + class AutomaticGroupMembership < ::Jobs::Base def execute(args) group_id = args[:group_id] diff --git a/app/jobs/regular/backup_chunks_merger.rb b/app/jobs/regular/backup_chunks_merger.rb index 35695d10a8..5deb37f333 100644 --- a/app/jobs/regular/backup_chunks_merger.rb +++ b/app/jobs/regular/backup_chunks_merger.rb @@ -1,11 +1,8 @@ # frozen_string_literal: true -require_dependency "backup_restore/local_backup_store" -require_dependency "backup_restore/backup_store" - module Jobs - class BackupChunksMerger < Jobs::Base + class BackupChunksMerger < ::Jobs::Base sidekiq_options queue: 'critical', retry: false def execute(args) diff --git a/app/jobs/regular/bulk_grant_trust_level.rb b/app/jobs/regular/bulk_grant_trust_level.rb index 886e604361..f61b747d20 100644 --- a/app/jobs/regular/bulk_grant_trust_level.rb +++ b/app/jobs/regular/bulk_grant_trust_level.rb @@ -2,7 +2,7 @@ module Jobs - class BulkGrantTrustLevel < Jobs::Base + class BulkGrantTrustLevel < ::Jobs::Base def execute(args) trust_level = args[:trust_level] diff --git a/app/jobs/regular/bulk_invite.rb b/app/jobs/regular/bulk_invite.rb index f7ccd44fd6..b5980a0db9 100644 --- a/app/jobs/regular/bulk_invite.rb +++ b/app/jobs/regular/bulk_invite.rb @@ -1,10 +1,7 @@ # frozen_string_literal: true -require_dependency 'system_message' - module Jobs - - class BulkInvite < Jobs::Base + class BulkInvite < ::Jobs::Base sidekiq_options retry: false def initialize diff --git a/app/jobs/regular/bump_topic.rb b/app/jobs/regular/bump_topic.rb index e8176316b1..e795592393 100644 --- a/app/jobs/regular/bump_topic.rb +++ b/app/jobs/regular/bump_topic.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class BumpTopic < Jobs::Base + class BumpTopic < ::Jobs::Base def execute(args) topic_timer = TopicTimer.find_by(id: args[:topic_timer_id] || args[:topic_status_update_id]) diff --git a/app/jobs/regular/confirm_sns_subscription.rb b/app/jobs/regular/confirm_sns_subscription.rb index d37fdcfd79..073747067b 100644 --- a/app/jobs/regular/confirm_sns_subscription.rb +++ b/app/jobs/regular/confirm_sns_subscription.rb @@ -2,7 +2,7 @@ module Jobs - class ConfirmSnsSubscription < Jobs::Base + class ConfirmSnsSubscription < ::Jobs::Base sidekiq_options retry: false def execute(args) diff --git a/app/jobs/regular/crawl_topic_link.rb b/app/jobs/regular/crawl_topic_link.rb index 36c10ff0c2..da7659ec53 100644 --- a/app/jobs/regular/crawl_topic_link.rb +++ b/app/jobs/regular/crawl_topic_link.rb @@ -3,11 +3,9 @@ require 'open-uri' require 'nokogiri' require 'excon' -require_dependency 'retrieve_title' -require_dependency 'topic_link' module Jobs - class CrawlTopicLink < Jobs::Base + class CrawlTopicLink < ::Jobs::Base sidekiq_options queue: 'low' diff --git a/app/jobs/regular/create_avatar_thumbnails.rb b/app/jobs/regular/create_avatar_thumbnails.rb index aa9e4711c1..540c5803d5 100644 --- a/app/jobs/regular/create_avatar_thumbnails.rb +++ b/app/jobs/regular/create_avatar_thumbnails.rb @@ -2,7 +2,7 @@ module Jobs - class CreateAvatarThumbnails < Jobs::Base + class CreateAvatarThumbnails < ::Jobs::Base sidekiq_options queue: 'low' def execute(args) diff --git a/app/jobs/regular/create_backup.rb b/app/jobs/regular/create_backup.rb index 62c67575c1..dcd3a466f5 100644 --- a/app/jobs/regular/create_backup.rb +++ b/app/jobs/regular/create_backup.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require "backup_restore/backup_restore" +require "backup_restore" module Jobs - class CreateBackup < Jobs::Base + class CreateBackup < ::Jobs::Base sidekiq_options retry: false def execute(args) diff --git a/app/jobs/regular/create_user_reviewable.rb b/app/jobs/regular/create_user_reviewable.rb index f68902ec2b..58d0c8e15a 100644 --- a/app/jobs/regular/create_user_reviewable.rb +++ b/app/jobs/regular/create_user_reviewable.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Jobs::CreateUserReviewable < Jobs::Base +class Jobs::CreateUserReviewable < ::Jobs::Base attr_reader :reviewable def execute(args) diff --git a/app/jobs/regular/critical_user_email.rb b/app/jobs/regular/critical_user_email.rb index 7592c7244b..469729e77f 100644 --- a/app/jobs/regular/critical_user_email.rb +++ b/app/jobs/regular/critical_user_email.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true # base.rb uses this style of require, so maintain usage of it here -require_dependency "#{Rails.root}/app/jobs/regular/user_email.rb" module Jobs class CriticalUserEmail < UserEmail diff --git a/app/jobs/regular/delete_inaccessible_notifications.rb b/app/jobs/regular/delete_inaccessible_notifications.rb index ca38dc4ec8..e33bfd6fb1 100644 --- a/app/jobs/regular/delete_inaccessible_notifications.rb +++ b/app/jobs/regular/delete_inaccessible_notifications.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class DeleteInaccessibleNotifications < Jobs::Base + class DeleteInaccessibleNotifications < ::Jobs::Base def execute(args) raise Discourse::InvalidParameters.new(:topic_id) if args[:topic_id].blank? diff --git a/app/jobs/regular/delete_topic.rb b/app/jobs/regular/delete_topic.rb index 9b07166862..be6ab194e7 100644 --- a/app/jobs/regular/delete_topic.rb +++ b/app/jobs/regular/delete_topic.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class DeleteTopic < Jobs::Base + class DeleteTopic < ::Jobs::Base def execute(args) topic_timer = TopicTimer.find_by(id: args[:topic_timer_id] || args[:topic_status_update_id]) diff --git a/app/jobs/regular/download_avatar_from_url.rb b/app/jobs/regular/download_avatar_from_url.rb index d87baf0e27..e1864c80b3 100644 --- a/app/jobs/regular/download_avatar_from_url.rb +++ b/app/jobs/regular/download_avatar_from_url.rb @@ -2,7 +2,7 @@ module Jobs - class DownloadAvatarFromUrl < Jobs::Base + class DownloadAvatarFromUrl < ::Jobs::Base sidekiq_options retry: false def execute(args) diff --git a/app/jobs/regular/download_backup_email.rb b/app/jobs/regular/download_backup_email.rb index 73c31347c7..0977098e97 100644 --- a/app/jobs/regular/download_backup_email.rb +++ b/app/jobs/regular/download_backup_email.rb @@ -1,11 +1,8 @@ # frozen_string_literal: true -require_dependency 'email/sender' -require_dependency "email_backup_token" - module Jobs - class DownloadBackupEmail < Jobs::Base + class DownloadBackupEmail < ::Jobs::Base sidekiq_options queue: 'critical' diff --git a/app/jobs/regular/download_profile_background_from_url.rb b/app/jobs/regular/download_profile_background_from_url.rb index 554c7425c1..5d39c3d8c8 100644 --- a/app/jobs/regular/download_profile_background_from_url.rb +++ b/app/jobs/regular/download_profile_background_from_url.rb @@ -2,7 +2,7 @@ module Jobs - class DownloadProfileBackgroundFromUrl < Jobs::Base + class DownloadProfileBackgroundFromUrl < ::Jobs::Base sidekiq_options retry: false def execute(args) diff --git a/app/jobs/regular/emit_web_hook_event.rb b/app/jobs/regular/emit_web_hook_event.rb index 5160fd7dff..fd7f8ae2a7 100644 --- a/app/jobs/regular/emit_web_hook_event.rb +++ b/app/jobs/regular/emit_web_hook_event.rb @@ -3,7 +3,7 @@ require 'excon' module Jobs - class EmitWebHookEvent < Jobs::Base + class EmitWebHookEvent < ::Jobs::Base PING_EVENT = 'ping'.freeze MAX_RETRY_COUNT = 4.freeze RETRY_BACKOFF = 5 diff --git a/app/jobs/regular/enable_bootstrap_mode.rb b/app/jobs/regular/enable_bootstrap_mode.rb index 6deaef171b..bf804e24db 100644 --- a/app/jobs/regular/enable_bootstrap_mode.rb +++ b/app/jobs/regular/enable_bootstrap_mode.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class EnableBootstrapMode < Jobs::Base + class EnableBootstrapMode < ::Jobs::Base sidekiq_options queue: 'critical' def execute(args) diff --git a/app/jobs/regular/export_csv_file.rb b/app/jobs/regular/export_csv_file.rb index cea8c1ec81..2ab94729cf 100644 --- a/app/jobs/regular/export_csv_file.rb +++ b/app/jobs/regular/export_csv_file.rb @@ -1,14 +1,10 @@ # frozen_string_literal: true require 'csv' -require 'zip' -require_dependency 'system_message' -require_dependency 'upload_creator' -require_dependency 'upload_markdown' module Jobs - class ExportCsvFile < Jobs::Base + class ExportCsvFile < ::Jobs::Base sidekiq_options retry: false HEADER_ATTRS_FOR ||= HashWithIndifferentAccess.new( @@ -54,14 +50,14 @@ module Jobs FileUtils.mkdir_p(UserExport.base_directory) unless Dir.exists?(UserExport.base_directory) # Generate a compressed CSV file - csv_to_export = CSV.generate do |csv| - csv << get_header if @entity != "report" - public_send(export_method).each { |d| csv << d } - end - - compressed_file_path = "#{absolute_path}.zip" - Zip::File.open(compressed_file_path, Zip::File::CREATE) do |zipfile| - zipfile.get_output_stream(file_name) { |f| f.puts csv_to_export } + begin + CSV.open(absolute_path, "w") do |csv| + csv << get_header if @entity != "report" + public_send(export_method).each { |d| csv << d } + end + compressed_file_path = Compression::Zip.new.compress(UserExport.base_directory, file_name) + ensure + File.delete(absolute_path) end # create upload @@ -79,7 +75,7 @@ module Jobs if upload.persisted? user_export.update_columns(upload_id: upload.id) else - Rails.logger.warn("Failed to upload the file #{Discourse.base_uri}/export_csv/#{file_name}.gz") + Rails.logger.warn("Failed to upload the file #{compressed_file_path}") end end diff --git a/app/jobs/regular/feature_topic_users.rb b/app/jobs/regular/feature_topic_users.rb index 8ab797d7c5..84210a6baf 100644 --- a/app/jobs/regular/feature_topic_users.rb +++ b/app/jobs/regular/feature_topic_users.rb @@ -2,7 +2,7 @@ module Jobs - class FeatureTopicUsers < Jobs::Base + class FeatureTopicUsers < ::Jobs::Base def execute(args) topic_id = args[:topic_id] diff --git a/app/jobs/regular/invite_email.rb b/app/jobs/regular/invite_email.rb index c91e67ae35..b8278ff40a 100644 --- a/app/jobs/regular/invite_email.rb +++ b/app/jobs/regular/invite_email.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -require_dependency 'email/sender' - module Jobs # Asynchronously send an email - class InviteEmail < Jobs::Base + class InviteEmail < ::Jobs::Base def execute(args) raise Discourse::InvalidParameters.new(:invite_id) unless args[:invite_id].present? diff --git a/app/jobs/regular/invite_password_instructions_email.rb b/app/jobs/regular/invite_password_instructions_email.rb index 009a6b3792..14ccec97c4 100644 --- a/app/jobs/regular/invite_password_instructions_email.rb +++ b/app/jobs/regular/invite_password_instructions_email.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -require_dependency 'email/sender' - module Jobs # Asynchronously send an email - class InvitePasswordInstructionsEmail < Jobs::Base + class InvitePasswordInstructionsEmail < ::Jobs::Base def execute(args) raise Discourse::InvalidParameters.new(:username) unless args[:username].present? diff --git a/app/jobs/regular/notify_category_change.rb b/app/jobs/regular/notify_category_change.rb index c37cf44215..bac94a20e0 100644 --- a/app/jobs/regular/notify_category_change.rb +++ b/app/jobs/regular/notify_category_change.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true -require_dependency "post_alerter" - module Jobs - class NotifyCategoryChange < Jobs::Base + class NotifyCategoryChange < ::Jobs::Base def execute(args) post = Post.find_by(id: args[:post_id]) diff --git a/app/jobs/regular/notify_mailing_list_subscribers.rb b/app/jobs/regular/notify_mailing_list_subscribers.rb index 50230a380b..ba63d13af2 100644 --- a/app/jobs/regular/notify_mailing_list_subscribers.rb +++ b/app/jobs/regular/notify_mailing_list_subscribers.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true -require_dependency 'post' - module Jobs - class NotifyMailingListSubscribers < Jobs::Base + class NotifyMailingListSubscribers < ::Jobs::Base include Skippable RETRY_TIMES = [5.minute, 15.minute, 30.minute, 45.minute, 90.minute, 180.minute, 300.minute] diff --git a/app/jobs/regular/notify_moved_posts.rb b/app/jobs/regular/notify_moved_posts.rb index e12af831fe..f02feec7a6 100644 --- a/app/jobs/regular/notify_moved_posts.rb +++ b/app/jobs/regular/notify_moved_posts.rb @@ -2,7 +2,7 @@ module Jobs - class NotifyMovedPosts < Jobs::Base + class NotifyMovedPosts < ::Jobs::Base def execute(args) raise Discourse::InvalidParameters.new(:post_ids) if args[:post_ids].blank? diff --git a/app/jobs/regular/notify_post_revision.rb b/app/jobs/regular/notify_post_revision.rb index 2331c6655a..8d2160699c 100644 --- a/app/jobs/regular/notify_post_revision.rb +++ b/app/jobs/regular/notify_post_revision.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class NotifyPostRevision < Jobs::Base + class NotifyPostRevision < ::Jobs::Base def execute(args) raise Discourse::InvalidParameters.new(:user_ids) unless args[:user_ids] diff --git a/app/jobs/regular/notify_reviewable.rb b/app/jobs/regular/notify_reviewable.rb index 8bed238b54..589b09510e 100644 --- a/app/jobs/regular/notify_reviewable.rb +++ b/app/jobs/regular/notify_reviewable.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Jobs::NotifyReviewable < Jobs::Base +class Jobs::NotifyReviewable < ::Jobs::Base def execute(args) reviewable = Reviewable.find_by(id: args[:reviewable_id]) diff --git a/app/jobs/regular/notify_tag_change.rb b/app/jobs/regular/notify_tag_change.rb index 370227eef7..4725896cd4 100644 --- a/app/jobs/regular/notify_tag_change.rb +++ b/app/jobs/regular/notify_tag_change.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true -require_dependency "post_alerter" - module Jobs - class NotifyTagChange < Jobs::Base + class NotifyTagChange < ::Jobs::Base def execute(args) post = Post.find_by(id: args[:post_id]) diff --git a/app/jobs/regular/post_alert.rb b/app/jobs/regular/post_alert.rb index 4c28b95947..dc6b8b1440 100644 --- a/app/jobs/regular/post_alert.rb +++ b/app/jobs/regular/post_alert.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true -require_dependency 'post_alerter' - module Jobs - class PostAlert < Jobs::Base + class PostAlert < ::Jobs::Base def execute(args) post = Post.find_by(id: args[:post_id]) diff --git a/app/jobs/regular/process_bulk_invite_emails.rb b/app/jobs/regular/process_bulk_invite_emails.rb index f4d057e7c2..c7901d29c0 100644 --- a/app/jobs/regular/process_bulk_invite_emails.rb +++ b/app/jobs/regular/process_bulk_invite_emails.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true -require_dependency 'email/sender' - module Jobs - class ProcessBulkInviteEmails < Jobs::Base + class ProcessBulkInviteEmails < ::Jobs::Base def execute(args) pending_invite_ids = Invite.where(emailed_status: Invite.emailed_status_types[:bulk_pending]).limit(Invite::BULK_INVITE_EMAIL_LIMIT).pluck(:id) diff --git a/app/jobs/regular/process_email.rb b/app/jobs/regular/process_email.rb index d29b410dbb..f738fc5be3 100644 --- a/app/jobs/regular/process_email.rb +++ b/app/jobs/regular/process_email.rb @@ -2,7 +2,7 @@ module Jobs - class ProcessEmail < Jobs::Base + class ProcessEmail < ::Jobs::Base sidekiq_options retry: 3 def execute(args) diff --git a/app/jobs/regular/process_post.rb b/app/jobs/regular/process_post.rb index c2eea3e3ad..266072de01 100644 --- a/app/jobs/regular/process_post.rb +++ b/app/jobs/regular/process_post.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true require 'image_sizer' -require_dependency 'cooked_post_processor' module Jobs - class ProcessPost < Jobs::Base + class ProcessPost < ::Jobs::Base def execute(args) post = Post.find_by(id: args[:post_id]) diff --git a/app/jobs/regular/process_sns_notification.rb b/app/jobs/regular/process_sns_notification.rb index be51110dd0..ff25dc83fe 100644 --- a/app/jobs/regular/process_sns_notification.rb +++ b/app/jobs/regular/process_sns_notification.rb @@ -2,7 +2,7 @@ module Jobs - class ProcessSnsNotification < Jobs::Base + class ProcessSnsNotification < ::Jobs::Base sidekiq_options retry: false def execute(args) diff --git a/app/jobs/regular/publish_topic_to_category.rb b/app/jobs/regular/publish_topic_to_category.rb index 7e26fdd8c6..1b86fc9034 100644 --- a/app/jobs/regular/publish_topic_to_category.rb +++ b/app/jobs/regular/publish_topic_to_category.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true -require_dependency 'topic_publisher' - module Jobs - class PublishTopicToCategory < Jobs::Base + class PublishTopicToCategory < ::Jobs::Base def execute(args) topic_timer = TopicTimer.find_by(id: args[:topic_timer_id] || args[:topic_status_update_id]) return if topic_timer.blank? diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb index c741b659d4..0b4823b54d 100644 --- a/app/jobs/regular/pull_hotlinked_images.rb +++ b/app/jobs/regular/pull_hotlinked_images.rb @@ -1,12 +1,8 @@ # frozen_string_literal: true -require_dependency 'url_helper' -require_dependency 'file_helper' -require_dependency 'upload_creator' - module Jobs - class PullHotlinkedImages < Jobs::Base + class PullHotlinkedImages < ::Jobs::Base sidekiq_options queue: 'low' def initialize diff --git a/app/jobs/regular/push_notification.rb b/app/jobs/regular/push_notification.rb index 3a16d626d6..415152422e 100644 --- a/app/jobs/regular/push_notification.rb +++ b/app/jobs/regular/push_notification.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class PushNotification < Jobs::Base + class PushNotification < ::Jobs::Base def execute(args) notification = args["payload"] notification["url"] = UrlHelper.absolute_without_cdn(notification["post_url"]) @@ -20,6 +20,8 @@ module Jobs notification.merge(client_id: client_id) end + next unless push_url + result = Excon.post(push_url, body: payload.merge(notifications: notifications).to_json, headers: { 'Content-Type' => 'application/json', 'Accept' => 'application/json' } diff --git a/app/jobs/regular/rebake_custom_emoji_posts.rb b/app/jobs/regular/rebake_custom_emoji_posts.rb index dbf8fdfbca..492fa42f9b 100644 --- a/app/jobs/regular/rebake_custom_emoji_posts.rb +++ b/app/jobs/regular/rebake_custom_emoji_posts.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class RebakeCustomEmojiPosts < Jobs::Base + class RebakeCustomEmojiPosts < ::Jobs::Base def execute(args) Post.where("raw LIKE ?", "%:#{args[:name]}:%").find_each(&:rebake!) end diff --git a/app/jobs/regular/retrieve_topic.rb b/app/jobs/regular/retrieve_topic.rb index 5c41ebf866..ac3f0aacad 100644 --- a/app/jobs/regular/retrieve_topic.rb +++ b/app/jobs/regular/retrieve_topic.rb @@ -1,12 +1,9 @@ # frozen_string_literal: true -require_dependency 'email/sender' -require_dependency 'topic_retriever' - module Jobs # Asynchronously retrieve a topic from an embedded site - class RetrieveTopic < Jobs::Base + class RetrieveTopic < ::Jobs::Base def execute(args) raise Discourse::InvalidParameters.new(:embed_url) unless args[:embed_url].present? diff --git a/app/jobs/regular/run_heartbeat.rb b/app/jobs/regular/run_heartbeat.rb index 364343d043..708c45f7db 100644 --- a/app/jobs/regular/run_heartbeat.rb +++ b/app/jobs/regular/run_heartbeat.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class RunHeartbeat < Jobs::Base + class RunHeartbeat < ::Jobs::Base sidekiq_options queue: 'critical' diff --git a/app/jobs/regular/send_push_notification.rb b/app/jobs/regular/send_push_notification.rb index 9eb072da76..4fbd7c761b 100644 --- a/app/jobs/regular/send_push_notification.rb +++ b/app/jobs/regular/send_push_notification.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class SendPushNotification < Jobs::Base + class SendPushNotification < ::Jobs::Base def execute(args) user = User.find_by(id: args[:user_id]) PushNotificationPusher.push(user, args[:payload]) if user diff --git a/app/jobs/regular/send_system_message.rb b/app/jobs/regular/send_system_message.rb index 80fbc4e7e8..23ebe656b2 100644 --- a/app/jobs/regular/send_system_message.rb +++ b/app/jobs/regular/send_system_message.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true require 'image_sizer' -require_dependency 'system_message' module Jobs - class SendSystemMessage < Jobs::Base + class SendSystemMessage < ::Jobs::Base def execute(args) raise Discourse::InvalidParameters.new(:user_id) unless args[:user_id].present? diff --git a/app/jobs/regular/suspicious_login.rb b/app/jobs/regular/suspicious_login.rb index a2f15d32bc..4ba98be381 100644 --- a/app/jobs/regular/suspicious_login.rb +++ b/app/jobs/regular/suspicious_login.rb @@ -2,7 +2,7 @@ module Jobs - class SuspiciousLogin < Jobs::Base + class SuspiciousLogin < ::Jobs::Base def execute(args) if UserAuthToken.is_suspicious(args[:user_id], args[:client_ip]) diff --git a/app/jobs/regular/toggle_topic_closed.rb b/app/jobs/regular/toggle_topic_closed.rb index 96e631d066..246eda4db6 100644 --- a/app/jobs/regular/toggle_topic_closed.rb +++ b/app/jobs/regular/toggle_topic_closed.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class ToggleTopicClosed < Jobs::Base + class ToggleTopicClosed < ::Jobs::Base def execute(args) topic_timer = TopicTimer.find_by(id: args[:topic_timer_id] || args[:topic_status_update_id]) state = !!args[:state] diff --git a/app/jobs/regular/topic_action_converter.rb b/app/jobs/regular/topic_action_converter.rb index d047bc8361..dff892d06a 100644 --- a/app/jobs/regular/topic_action_converter.rb +++ b/app/jobs/regular/topic_action_converter.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Jobs::TopicActionConverter < Jobs::Base +class Jobs::TopicActionConverter < ::Jobs::Base # Re-creating all the user actions could be very slow, so let's do it in a job # to avoid a N+1 query on a front facing operation. diff --git a/app/jobs/regular/topic_reminder.rb b/app/jobs/regular/topic_reminder.rb index 5b63e11fd4..a9eed2b36e 100644 --- a/app/jobs/regular/topic_reminder.rb +++ b/app/jobs/regular/topic_reminder.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class TopicReminder < Jobs::Base + class TopicReminder < ::Jobs::Base def execute(args) topic_timer = TopicTimer.find_by(id: args[:topic_timer_id]) diff --git a/app/jobs/regular/truncate_user_flag_stats.rb b/app/jobs/regular/truncate_user_flag_stats.rb index 30dae8695a..d75f7bb5aa 100644 --- a/app/jobs/regular/truncate_user_flag_stats.rb +++ b/app/jobs/regular/truncate_user_flag_stats.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Jobs::TruncateUserFlagStats < Jobs::Base +class Jobs::TruncateUserFlagStats < ::Jobs::Base def self.truncate_to 100 diff --git a/app/jobs/regular/unpin_topic.rb b/app/jobs/regular/unpin_topic.rb index 8dc0fe439b..b99e505904 100644 --- a/app/jobs/regular/unpin_topic.rb +++ b/app/jobs/regular/unpin_topic.rb @@ -2,7 +2,7 @@ module Jobs - class UnpinTopic < Jobs::Base + class UnpinTopic < ::Jobs::Base def execute(args) topic_id = args[:topic_id] diff --git a/app/jobs/regular/update_gravatar.rb b/app/jobs/regular/update_gravatar.rb index 795018c57f..7ae721759d 100644 --- a/app/jobs/regular/update_gravatar.rb +++ b/app/jobs/regular/update_gravatar.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true module Jobs - - class UpdateGravatar < Jobs::Base + class UpdateGravatar < ::Jobs::Base sidekiq_options queue: 'low' diff --git a/app/jobs/regular/update_group_mentions.rb b/app/jobs/regular/update_group_mentions.rb index a26151b073..4df6b9145f 100644 --- a/app/jobs/regular/update_group_mentions.rb +++ b/app/jobs/regular/update_group_mentions.rb @@ -2,7 +2,7 @@ module Jobs - class UpdateGroupMentions < Jobs::Base + class UpdateGroupMentions < ::Jobs::Base def execute(args) group = Group.find_by(id: args[:group_id]) diff --git a/app/jobs/regular/update_private_uploads_acl.rb b/app/jobs/regular/update_private_uploads_acl.rb index 80a6993dc7..4437ea9649 100644 --- a/app/jobs/regular/update_private_uploads_acl.rb +++ b/app/jobs/regular/update_private_uploads_acl.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class UpdatePrivateUploadsAcl < Jobs::Base + class UpdatePrivateUploadsAcl < ::Jobs::Base # only runs when SiteSetting.prevent_anons_from_downloading_files is updated def execute(args) return if !SiteSetting.enable_s3_uploads diff --git a/app/jobs/regular/update_s3_inventory.rb b/app/jobs/regular/update_s3_inventory.rb index dae3b06a56..2d279268f0 100644 --- a/app/jobs/regular/update_s3_inventory.rb +++ b/app/jobs/regular/update_s3_inventory.rb @@ -4,7 +4,7 @@ require "s3_inventory" module Jobs # if upload bucket changes or inventory bucket changes we want to update s3 bucket policy and inventory configuration - class UpdateS3Inventory < Jobs::Base + class UpdateS3Inventory < ::Jobs::Base def execute(args) return unless SiteSetting.enable_s3_inventory? && SiteSetting.Upload.enable_s3_uploads diff --git a/app/jobs/regular/update_top_redirection.rb b/app/jobs/regular/update_top_redirection.rb index 706fc2e697..80ad6acefb 100644 --- a/app/jobs/regular/update_top_redirection.rb +++ b/app/jobs/regular/update_top_redirection.rb @@ -2,7 +2,7 @@ module Jobs - class UpdateTopRedirection < Jobs::Base + class UpdateTopRedirection < ::Jobs::Base def execute(args) return if args[:user_id].blank? || args[:redirected_at].blank? diff --git a/app/jobs/regular/update_username.rb b/app/jobs/regular/update_username.rb index 6fdf60fa17..308dc75c99 100644 --- a/app/jobs/regular/update_username.rb +++ b/app/jobs/regular/update_username.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class UpdateUsername < Jobs::Base + class UpdateUsername < ::Jobs::Base sidekiq_options queue: 'low' diff --git a/app/jobs/regular/user_email.rb b/app/jobs/regular/user_email.rb index 447872fd7f..5bdea4fb86 100644 --- a/app/jobs/regular/user_email.rb +++ b/app/jobs/regular/user_email.rb @@ -1,12 +1,9 @@ # frozen_string_literal: true -require_dependency 'email/sender' -require_dependency 'user_notifications' - module Jobs # Asynchronously send an email to a user - class UserEmail < Jobs::Base + class UserEmail < ::Jobs::Base include Skippable sidekiq_options queue: 'low' diff --git a/app/jobs/scheduled/about_stats.rb b/app/jobs/scheduled/about_stats.rb index 998acf1db5..adf40c613b 100644 --- a/app/jobs/scheduled/about_stats.rb +++ b/app/jobs/scheduled/about_stats.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class AboutStats < Jobs::Scheduled + class AboutStats < ::Jobs::Scheduled every 30.minutes def execute(args) diff --git a/app/jobs/scheduled/activation_reminder_emails.rb b/app/jobs/scheduled/activation_reminder_emails.rb index 5151bec780..df0b1f86ec 100644 --- a/app/jobs/scheduled/activation_reminder_emails.rb +++ b/app/jobs/scheduled/activation_reminder_emails.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class ActivationReminderEmails < Jobs::Scheduled + class ActivationReminderEmails < ::Jobs::Scheduled every 2.hours def execute(args) diff --git a/app/jobs/scheduled/auto_expire_user_api_keys.rb b/app/jobs/scheduled/auto_expire_user_api_keys.rb index 7217608ef4..4a4e041f7c 100644 --- a/app/jobs/scheduled/auto_expire_user_api_keys.rb +++ b/app/jobs/scheduled/auto_expire_user_api_keys.rb @@ -2,7 +2,7 @@ module Jobs - class AutoExpireUserApiKeys < Jobs::Scheduled + class AutoExpireUserApiKeys < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/auto_queue_handler.rb b/app/jobs/scheduled/auto_queue_handler.rb index 659aa92c64..500a45b079 100644 --- a/app/jobs/scheduled/auto_queue_handler.rb +++ b/app/jobs/scheduled/auto_queue_handler.rb @@ -3,7 +3,7 @@ # This job will automatically act on records that have gone unhandled on a # queue for a long time. module Jobs - class AutoQueueHandler < Jobs::Scheduled + class AutoQueueHandler < ::Jobs::Scheduled every 1.day diff --git a/app/jobs/scheduled/badge_grant.rb b/app/jobs/scheduled/badge_grant.rb index ccddc6d582..b4fd367c69 100644 --- a/app/jobs/scheduled/badge_grant.rb +++ b/app/jobs/scheduled/badge_grant.rb @@ -2,7 +2,7 @@ module Jobs - class BadgeGrant < Jobs::Scheduled + class BadgeGrant < ::Jobs::Scheduled def self.run self.new.execute(nil) end diff --git a/app/jobs/scheduled/category_stats.rb b/app/jobs/scheduled/category_stats.rb index 44843a67e3..b1de99224e 100644 --- a/app/jobs/scheduled/category_stats.rb +++ b/app/jobs/scheduled/category_stats.rb @@ -2,7 +2,7 @@ module Jobs - class CategoryStats < Jobs::Scheduled + class CategoryStats < ::Jobs::Scheduled every 24.hours def execute(args) diff --git a/app/jobs/scheduled/check_out_of_date_themes.rb b/app/jobs/scheduled/check_out_of_date_themes.rb index 31f8b1547a..1003e25a90 100644 --- a/app/jobs/scheduled/check_out_of_date_themes.rb +++ b/app/jobs/scheduled/check_out_of_date_themes.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class CheckOutOfDateThemes < Jobs::Scheduled + class CheckOutOfDateThemes < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/clean_up_associated_accounts.rb b/app/jobs/scheduled/clean_up_associated_accounts.rb index 0d009d2fee..ac2ba4802b 100644 --- a/app/jobs/scheduled/clean_up_associated_accounts.rb +++ b/app/jobs/scheduled/clean_up_associated_accounts.rb @@ -2,7 +2,7 @@ module Jobs - class CleanUpAssociatedAccounts < Jobs::Scheduled + class CleanUpAssociatedAccounts < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/clean_up_crawler_stats.rb b/app/jobs/scheduled/clean_up_crawler_stats.rb index 40dc386f8d..09f704bd34 100644 --- a/app/jobs/scheduled/clean_up_crawler_stats.rb +++ b/app/jobs/scheduled/clean_up_crawler_stats.rb @@ -2,7 +2,7 @@ module Jobs - class CleanUpCrawlerStats < Jobs::Scheduled + class CleanUpCrawlerStats < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/clean_up_deprecated_url_site_settings.rb b/app/jobs/scheduled/clean_up_deprecated_url_site_settings.rb index b7c4fc212d..6352c92857 100644 --- a/app/jobs/scheduled/clean_up_deprecated_url_site_settings.rb +++ b/app/jobs/scheduled/clean_up_deprecated_url_site_settings.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class CleanUpDeprecatedUrlSiteSettings < Jobs::Scheduled + class CleanUpDeprecatedUrlSiteSettings < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/clean_up_email_logs.rb b/app/jobs/scheduled/clean_up_email_logs.rb index 083f9b8b36..9799764e71 100644 --- a/app/jobs/scheduled/clean_up_email_logs.rb +++ b/app/jobs/scheduled/clean_up_email_logs.rb @@ -2,7 +2,7 @@ module Jobs - class CleanUpEmailLogs < Jobs::Scheduled + class CleanUpEmailLogs < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/clean_up_exports.rb b/app/jobs/scheduled/clean_up_exports.rb index 8352aa105a..7a29ff2eb6 100644 --- a/app/jobs/scheduled/clean_up_exports.rb +++ b/app/jobs/scheduled/clean_up_exports.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class CleanUpExports < Jobs::Scheduled + class CleanUpExports < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/clean_up_inactive_users.rb b/app/jobs/scheduled/clean_up_inactive_users.rb index f0ccc89a99..a0a4ab5f61 100644 --- a/app/jobs/scheduled/clean_up_inactive_users.rb +++ b/app/jobs/scheduled/clean_up_inactive_users.rb @@ -2,7 +2,7 @@ module Jobs - class CleanUpInactiveUsers < Jobs::Scheduled + class CleanUpInactiveUsers < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/clean_up_post_reply_keys.rb b/app/jobs/scheduled/clean_up_post_reply_keys.rb index a520061ebd..748da603f3 100644 --- a/app/jobs/scheduled/clean_up_post_reply_keys.rb +++ b/app/jobs/scheduled/clean_up_post_reply_keys.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class CleanUpPostReplyKeys < Jobs::Scheduled + class CleanUpPostReplyKeys < ::Jobs::Scheduled every 1.day def execute(_) diff --git a/app/jobs/scheduled/clean_up_search_logs.rb b/app/jobs/scheduled/clean_up_search_logs.rb index 0f04764197..03f934bee7 100644 --- a/app/jobs/scheduled/clean_up_search_logs.rb +++ b/app/jobs/scheduled/clean_up_search_logs.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class CleanUpSearchLogs < Jobs::Scheduled + class CleanUpSearchLogs < ::Jobs::Scheduled every 1.week def execute(args) diff --git a/app/jobs/scheduled/clean_up_unmatched_emails.rb b/app/jobs/scheduled/clean_up_unmatched_emails.rb index 6ce5299255..58fb112e17 100644 --- a/app/jobs/scheduled/clean_up_unmatched_emails.rb +++ b/app/jobs/scheduled/clean_up_unmatched_emails.rb @@ -2,7 +2,7 @@ module Jobs - class CleanUpUnmatchedEmails < Jobs::Scheduled + class CleanUpUnmatchedEmails < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/clean_up_unmatched_ips.rb b/app/jobs/scheduled/clean_up_unmatched_ips.rb index f535f5b450..fa16315f2d 100644 --- a/app/jobs/scheduled/clean_up_unmatched_ips.rb +++ b/app/jobs/scheduled/clean_up_unmatched_ips.rb @@ -2,7 +2,7 @@ module Jobs - class CleanUpUnmatchedIPs < Jobs::Scheduled + class CleanUpUnmatchedIPs < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/clean_up_unsubscribe_keys.rb b/app/jobs/scheduled/clean_up_unsubscribe_keys.rb index 70f6b99473..ed8dfb5566 100644 --- a/app/jobs/scheduled/clean_up_unsubscribe_keys.rb +++ b/app/jobs/scheduled/clean_up_unsubscribe_keys.rb @@ -2,7 +2,7 @@ module Jobs - class CleanUpUnsubscribeKeys < Jobs::Scheduled + class CleanUpUnsubscribeKeys < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/clean_up_unused_staged_users.rb b/app/jobs/scheduled/clean_up_unused_staged_users.rb index ffcac8a0af..3769dffe6b 100644 --- a/app/jobs/scheduled/clean_up_unused_staged_users.rb +++ b/app/jobs/scheduled/clean_up_unused_staged_users.rb @@ -2,7 +2,7 @@ module Jobs - class CleanUpUnusedStagedUsers < Jobs::Scheduled + class CleanUpUnusedStagedUsers < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/clean_up_uploads.rb b/app/jobs/scheduled/clean_up_uploads.rb index a6ff2e3a6b..97af8b0240 100644 --- a/app/jobs/scheduled/clean_up_uploads.rb +++ b/app/jobs/scheduled/clean_up_uploads.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class CleanUpUploads < Jobs::Scheduled + class CleanUpUploads < ::Jobs::Scheduled every 1.hour def execute(args) diff --git a/app/jobs/scheduled/create_missing_avatars.rb b/app/jobs/scheduled/create_missing_avatars.rb index 42e5365d41..0847b84f2e 100644 --- a/app/jobs/scheduled/create_missing_avatars.rb +++ b/app/jobs/scheduled/create_missing_avatars.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class CreateMissingAvatars < Jobs::Scheduled + class CreateMissingAvatars < ::Jobs::Scheduled every 1.hour def execute(args) diff --git a/app/jobs/scheduled/dashboard_stats.rb b/app/jobs/scheduled/dashboard_stats.rb index 26cc43afff..c256466642 100644 --- a/app/jobs/scheduled/dashboard_stats.rb +++ b/app/jobs/scheduled/dashboard_stats.rb @@ -1,11 +1,7 @@ # frozen_string_literal: true -require_dependency 'admin_dashboard_data' -require_dependency 'group' -require_dependency 'group_message' - module Jobs - class DashboardStats < Jobs::Scheduled + class DashboardStats < ::Jobs::Scheduled every 30.minutes def execute(args) diff --git a/app/jobs/scheduled/destroy_old_deletion_stubs.rb b/app/jobs/scheduled/destroy_old_deletion_stubs.rb index 8624b7ea48..0762bbbaac 100644 --- a/app/jobs/scheduled/destroy_old_deletion_stubs.rb +++ b/app/jobs/scheduled/destroy_old_deletion_stubs.rb @@ -2,7 +2,7 @@ module Jobs # various consistency checks - class DestroyOldDeletionStubs < Jobs::Scheduled + class DestroyOldDeletionStubs < ::Jobs::Scheduled every 30.minutes def execute(args) diff --git a/app/jobs/scheduled/destroy_old_hidden_posts.rb b/app/jobs/scheduled/destroy_old_hidden_posts.rb index ee7819240c..fa282fcbdb 100644 --- a/app/jobs/scheduled/destroy_old_hidden_posts.rb +++ b/app/jobs/scheduled/destroy_old_hidden_posts.rb @@ -2,7 +2,7 @@ module Jobs - class DestroyOldHiddenPosts < Jobs::Scheduled + class DestroyOldHiddenPosts < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/directory_refresh_daily.rb b/app/jobs/scheduled/directory_refresh_daily.rb index 90311bbad2..4840f23f22 100644 --- a/app/jobs/scheduled/directory_refresh_daily.rb +++ b/app/jobs/scheduled/directory_refresh_daily.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class DirectoryRefreshDaily < Jobs::Scheduled + class DirectoryRefreshDaily < ::Jobs::Scheduled every 1.hour def execute(args) diff --git a/app/jobs/scheduled/directory_refresh_older.rb b/app/jobs/scheduled/directory_refresh_older.rb index 523be14b2f..a1f792cc44 100644 --- a/app/jobs/scheduled/directory_refresh_older.rb +++ b/app/jobs/scheduled/directory_refresh_older.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class DirectoryRefreshOlder < Jobs::Scheduled + class DirectoryRefreshOlder < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/disable_bootstrap_mode.rb b/app/jobs/scheduled/disable_bootstrap_mode.rb index e41cf8577b..61650b3094 100644 --- a/app/jobs/scheduled/disable_bootstrap_mode.rb +++ b/app/jobs/scheduled/disable_bootstrap_mode.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class DisableBootstrapMode < Jobs::Scheduled + class DisableBootstrapMode < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/enqueue_digest_emails.rb b/app/jobs/scheduled/enqueue_digest_emails.rb index e8db948ca6..59b091263c 100644 --- a/app/jobs/scheduled/enqueue_digest_emails.rb +++ b/app/jobs/scheduled/enqueue_digest_emails.rb @@ -2,7 +2,7 @@ module Jobs - class EnqueueDigestEmails < Jobs::Scheduled + class EnqueueDigestEmails < ::Jobs::Scheduled every 30.minutes def execute(args) diff --git a/app/jobs/scheduled/enqueue_onceoffs.rb b/app/jobs/scheduled/enqueue_onceoffs.rb index d529fad062..29692c6311 100644 --- a/app/jobs/scheduled/enqueue_onceoffs.rb +++ b/app/jobs/scheduled/enqueue_onceoffs.rb @@ -2,11 +2,11 @@ module Jobs - class EnqueueOnceoffs < Jobs::Scheduled + class EnqueueOnceoffs < ::Jobs::Scheduled every 10.minutes def execute(args) - Jobs::Onceoff.enqueue_all + ::Jobs::Onceoff.enqueue_all end end diff --git a/app/jobs/scheduled/ensure_db_consistency.rb b/app/jobs/scheduled/ensure_db_consistency.rb index 4d602187ae..6d7f3fbbb8 100644 --- a/app/jobs/scheduled/ensure_db_consistency.rb +++ b/app/jobs/scheduled/ensure_db_consistency.rb @@ -2,7 +2,7 @@ module Jobs # various consistency checks - class EnsureDbConsistency < Jobs::Scheduled + class EnsureDbConsistency < ::Jobs::Scheduled every 12.hours def execute(args) diff --git a/app/jobs/scheduled/ensure_s3_uploads_existence.rb b/app/jobs/scheduled/ensure_s3_uploads_existence.rb index df29c38527..ccde386f3e 100644 --- a/app/jobs/scheduled/ensure_s3_uploads_existence.rb +++ b/app/jobs/scheduled/ensure_s3_uploads_existence.rb @@ -2,7 +2,7 @@ module Jobs - class EnsureS3UploadsExistence < Jobs::Scheduled + class EnsureS3UploadsExistence < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/fix_user_usernames_and_groups_names_clash.rb b/app/jobs/scheduled/fix_user_usernames_and_groups_names_clash.rb index 4158e2e7a2..d176217d55 100644 --- a/app/jobs/scheduled/fix_user_usernames_and_groups_names_clash.rb +++ b/app/jobs/scheduled/fix_user_usernames_and_groups_names_clash.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class FixUserUsernamesAndGroupNamesClash < Jobs::Scheduled + class FixUserUsernamesAndGroupsNamesClash < ::Jobs::Scheduled every 1.week def execute(args) diff --git a/app/jobs/scheduled/grant_anniversary_badges.rb b/app/jobs/scheduled/grant_anniversary_badges.rb index a091c3032b..a5246c3243 100644 --- a/app/jobs/scheduled/grant_anniversary_badges.rb +++ b/app/jobs/scheduled/grant_anniversary_badges.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class GrantAnniversaryBadges < Jobs::Scheduled + class GrantAnniversaryBadges < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb b/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb index fe55007195..59fb77bc94 100644 --- a/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb +++ b/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true -require 'badge_granter' - module Jobs - class GrantNewUserOfTheMonthBadges < Jobs::Scheduled + class GrantNewUserOfTheMonthBadges < ::Jobs::Scheduled every 1.day MAX_AWARDED = 2 diff --git a/app/jobs/scheduled/heartbeat.rb b/app/jobs/scheduled/heartbeat.rb index c1b8a8cb24..0f0d9fc020 100644 --- a/app/jobs/scheduled/heartbeat.rb +++ b/app/jobs/scheduled/heartbeat.rb @@ -3,7 +3,7 @@ module Jobs # used to ensure at least 1 sidekiq is running correctly - class Heartbeat < Jobs::Scheduled + class Heartbeat < ::Jobs::Scheduled every 3.minute def execute(args) diff --git a/app/jobs/scheduled/ignored_users_summary.rb b/app/jobs/scheduled/ignored_users_summary.rb index c84122e0a2..59483c2b52 100644 --- a/app/jobs/scheduled/ignored_users_summary.rb +++ b/app/jobs/scheduled/ignored_users_summary.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class IgnoredUsersSummary < Jobs::Scheduled + class IgnoredUsersSummary < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/invalidate_inactive_admins.rb b/app/jobs/scheduled/invalidate_inactive_admins.rb index fbe8929d04..8b1468d762 100644 --- a/app/jobs/scheduled/invalidate_inactive_admins.rb +++ b/app/jobs/scheduled/invalidate_inactive_admins.rb @@ -2,7 +2,7 @@ module Jobs - class InvalidateInactiveAdmins < Jobs::Scheduled + class InvalidateInactiveAdmins < ::Jobs::Scheduled every 1.day def execute(_) diff --git a/app/jobs/scheduled/migrate_upload_scheme.rb b/app/jobs/scheduled/migrate_upload_scheme.rb index 6d4683ccd5..601ad3afd8 100644 --- a/app/jobs/scheduled/migrate_upload_scheme.rb +++ b/app/jobs/scheduled/migrate_upload_scheme.rb @@ -2,7 +2,7 @@ module Jobs - class MigrateUploadScheme < Jobs::Scheduled + class MigrateUploadScheme < ::Jobs::Scheduled every 10.minutes sidekiq_options retry: false diff --git a/app/jobs/scheduled/pending_queued_posts_reminder.rb b/app/jobs/scheduled/pending_queued_posts_reminder.rb index f0d3608889..599446374e 100644 --- a/app/jobs/scheduled/pending_queued_posts_reminder.rb +++ b/app/jobs/scheduled/pending_queued_posts_reminder.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class PendingQueuedPostReminder < Jobs::Scheduled + class PendingQueuedPostsReminder < ::Jobs::Scheduled every 1.hour diff --git a/app/jobs/scheduled/pending_reviewables_reminder.rb b/app/jobs/scheduled/pending_reviewables_reminder.rb index 92642b0327..f06e8a7ed2 100644 --- a/app/jobs/scheduled/pending_reviewables_reminder.rb +++ b/app/jobs/scheduled/pending_reviewables_reminder.rb @@ -2,7 +2,7 @@ module Jobs - class PendingReviewablesReminder < Jobs::Scheduled + class PendingReviewablesReminder < ::Jobs::Scheduled every 1.hour attr_reader :sent_reminder diff --git a/app/jobs/scheduled/pending_users_reminder.rb b/app/jobs/scheduled/pending_users_reminder.rb index bf11df2900..8ef1332485 100644 --- a/app/jobs/scheduled/pending_users_reminder.rb +++ b/app/jobs/scheduled/pending_users_reminder.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true -require_dependency 'admin_user_index_query' - module Jobs - class PendingUsersReminder < Jobs::Scheduled + class PendingUsersReminder < ::Jobs::Scheduled every 1.hour def execute(args) diff --git a/app/jobs/scheduled/periodical_updates.rb b/app/jobs/scheduled/periodical_updates.rb index 12c1414ef4..2ae53c951d 100644 --- a/app/jobs/scheduled/periodical_updates.rb +++ b/app/jobs/scheduled/periodical_updates.rb @@ -1,12 +1,10 @@ # frozen_string_literal: true -require_dependency 'score_calculator' - module Jobs # This job will run on a regular basis to update statistics and denormalized data. # If it does not run, the site will not function properly. - class PeriodicalUpdates < Jobs::Scheduled + class PeriodicalUpdates < ::Jobs::Scheduled every 15.minutes def self.should_update_long_topics? diff --git a/app/jobs/scheduled/poll_feed.rb b/app/jobs/scheduled/poll_feed.rb index 54f47578b3..14411da2a5 100644 --- a/app/jobs/scheduled/poll_feed.rb +++ b/app/jobs/scheduled/poll_feed.rb @@ -5,13 +5,9 @@ # require 'digest/sha1' require 'excon' -require_dependency 'final_destination' -require_dependency 'post_creator' -require_dependency 'post_revisor' -require_dependency 'encodings' module Jobs - class PollFeed < Jobs::Scheduled + class PollFeed < ::Jobs::Scheduled every 5.minutes sidekiq_options retry: false @@ -41,8 +37,6 @@ module Jobs def ensure_rss_loaded return if @@rss_loaded require 'rss' - require_dependency 'feed_item_accessor' - require_dependency 'feed_element_installer' @@rss_loaded = true end diff --git a/app/jobs/scheduled/poll_mailbox.rb b/app/jobs/scheduled/poll_mailbox.rb index 80b858a11a..2202899483 100644 --- a/app/jobs/scheduled/poll_mailbox.rb +++ b/app/jobs/scheduled/poll_mailbox.rb @@ -1,13 +1,9 @@ # frozen_string_literal: true require 'net/pop' -require_dependency 'email/receiver' -require_dependency 'email/processor' -require_dependency 'email/sender' -require_dependency 'email/message_builder' module Jobs - class PollMailbox < Jobs::Scheduled + class PollMailbox < ::Jobs::Scheduled every SiteSetting.pop3_polling_period_mins.minutes sidekiq_options retry: false diff --git a/app/jobs/scheduled/process_badge_backlog.rb b/app/jobs/scheduled/process_badge_backlog.rb index 370165d4b2..61136aae1f 100644 --- a/app/jobs/scheduled/process_badge_backlog.rb +++ b/app/jobs/scheduled/process_badge_backlog.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class ProcessBadgeBacklog < Jobs::Scheduled + class ProcessBadgeBacklog < ::Jobs::Scheduled every 1.minute def execute(args) BadgeGranter.process_queue! diff --git a/app/jobs/scheduled/purge_deleted_uploads.rb b/app/jobs/scheduled/purge_deleted_uploads.rb index 56497f1ab5..73c3de5054 100644 --- a/app/jobs/scheduled/purge_deleted_uploads.rb +++ b/app/jobs/scheduled/purge_deleted_uploads.rb @@ -2,7 +2,7 @@ module Jobs - class PurgeDeletedUploads < Jobs::Scheduled + class PurgeDeletedUploads < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/purge_expired_ignored_users.rb b/app/jobs/scheduled/purge_expired_ignored_users.rb index abf360b66a..96b10837a6 100644 --- a/app/jobs/scheduled/purge_expired_ignored_users.rb +++ b/app/jobs/scheduled/purge_expired_ignored_users.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class PurgeExpiredIgnoredUsers < Jobs::Scheduled + class PurgeExpiredIgnoredUsers < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/purge_old_web_hook_events.rb b/app/jobs/scheduled/purge_old_web_hook_events.rb index 53704bcaac..cf4a009410 100644 --- a/app/jobs/scheduled/purge_old_web_hook_events.rb +++ b/app/jobs/scheduled/purge_old_web_hook_events.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class PurgeOldWebHookEvents < Jobs::Scheduled + class PurgeOldWebHookEvents < ::Jobs::Scheduled every 1.day def execute(_) diff --git a/app/jobs/scheduled/purge_unactivated.rb b/app/jobs/scheduled/purge_unactivated.rb index c693822e3b..c221bd8178 100644 --- a/app/jobs/scheduled/purge_unactivated.rb +++ b/app/jobs/scheduled/purge_unactivated.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class PurgeUnactivated < Jobs::Scheduled + class PurgeUnactivated < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/reindex_search.rb b/app/jobs/scheduled/reindex_search.rb index 50ed5e0bf3..2ffb811094 100644 --- a/app/jobs/scheduled/reindex_search.rb +++ b/app/jobs/scheduled/reindex_search.rb @@ -2,7 +2,7 @@ module Jobs # if locale changes or search algorithm changes we may want to reindex stuff - class ReindexSearch < Jobs::Scheduled + class ReindexSearch < ::Jobs::Scheduled every 2.hours CLEANUP_GRACE_PERIOD = 1.day.ago diff --git a/app/jobs/scheduled/reviewable_priorities.rb b/app/jobs/scheduled/reviewable_priorities.rb index b1133a89e4..7e0f5bbd7e 100644 --- a/app/jobs/scheduled/reviewable_priorities.rb +++ b/app/jobs/scheduled/reviewable_priorities.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Jobs::ReviewablePriorities < Jobs::Scheduled +class Jobs::ReviewablePriorities < ::Jobs::Scheduled every 1.day # We need this many reviewables before we'll calculate priorities diff --git a/app/jobs/scheduled/schedule_backup.rb b/app/jobs/scheduled/schedule_backup.rb index a33cf3c34c..8b9d43aff4 100644 --- a/app/jobs/scheduled/schedule_backup.rb +++ b/app/jobs/scheduled/schedule_backup.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class ScheduleBackup < Jobs::Scheduled + class ScheduleBackup < ::Jobs::Scheduled daily at: 0.hours sidekiq_options retry: false diff --git a/app/jobs/scheduled/tl3_promotions.rb b/app/jobs/scheduled/tl3_promotions.rb index 2bd9613c59..848f409b9c 100644 --- a/app/jobs/scheduled/tl3_promotions.rb +++ b/app/jobs/scheduled/tl3_promotions.rb @@ -2,7 +2,7 @@ module Jobs - class Tl3Promotions < Jobs::Scheduled + class Tl3Promotions < ::Jobs::Scheduled daily at: 4.hours def execute(args) diff --git a/app/jobs/scheduled/top_refresh_older.rb b/app/jobs/scheduled/top_refresh_older.rb index e24ec87587..74878de8d5 100644 --- a/app/jobs/scheduled/top_refresh_older.rb +++ b/app/jobs/scheduled/top_refresh_older.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class TopRefreshOlder < Jobs::Scheduled + class TopRefreshOlder < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/top_refresh_today.rb b/app/jobs/scheduled/top_refresh_today.rb index 4259a95241..237ed37121 100644 --- a/app/jobs/scheduled/top_refresh_today.rb +++ b/app/jobs/scheduled/top_refresh_today.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class TopRefreshToday < Jobs::Scheduled + class TopRefreshToday < ::Jobs::Scheduled every 1.hour def execute(args) diff --git a/app/jobs/scheduled/unsilence_users.rb b/app/jobs/scheduled/unsilence_users.rb index 94c343f4c2..f6f5f5e0ff 100644 --- a/app/jobs/scheduled/unsilence_users.rb +++ b/app/jobs/scheduled/unsilence_users.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class UnsilenceUsers < Jobs::Scheduled + class UnsilenceUsers < ::Jobs::Scheduled every 15.minutes def execute(args) diff --git a/app/jobs/scheduled/update_heat_settings.rb b/app/jobs/scheduled/update_heat_settings.rb index 9dafbbf118..887df93e8e 100644 --- a/app/jobs/scheduled/update_heat_settings.rb +++ b/app/jobs/scheduled/update_heat_settings.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Jobs - class UpdateHeatSettings < Jobs::Scheduled + class UpdateHeatSettings < ::Jobs::Scheduled every 1.month def execute(args) diff --git a/app/jobs/scheduled/version_check.rb b/app/jobs/scheduled/version_check.rb index c52bb97d4b..d28d943d93 100644 --- a/app/jobs/scheduled/version_check.rb +++ b/app/jobs/scheduled/version_check.rb @@ -1,10 +1,7 @@ # frozen_string_literal: true -require_dependency 'discourse_hub' -require_dependency 'discourse_updates' - module Jobs - class VersionCheck < Jobs::Scheduled + class VersionCheck < ::Jobs::Scheduled every 1.day def execute(args) diff --git a/app/jobs/scheduled/weekly.rb b/app/jobs/scheduled/weekly.rb index 705cf60024..19895bfef6 100644 --- a/app/jobs/scheduled/weekly.rb +++ b/app/jobs/scheduled/weekly.rb @@ -1,12 +1,10 @@ # frozen_string_literal: true -require_dependency 'score_calculator' - module Jobs # This job will run on a regular basis to update statistics and denormalized data. # If it does not run, the site will not function properly. - class Weekly < Jobs::Scheduled + class Weekly < ::Jobs::Scheduled every 1.week def execute(args) diff --git a/app/mailers/admin_confirmation_mailer.rb b/app/mailers/admin_confirmation_mailer.rb index a546db497b..e7ff1cde99 100644 --- a/app/mailers/admin_confirmation_mailer.rb +++ b/app/mailers/admin_confirmation_mailer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'email/message_builder' - class AdminConfirmationMailer < ActionMailer::Base include Email::BuildEmailHelper diff --git a/app/mailers/download_backup_mailer.rb b/app/mailers/download_backup_mailer.rb index fe291f0614..62d5e58368 100644 --- a/app/mailers/download_backup_mailer.rb +++ b/app/mailers/download_backup_mailer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'email/message_builder' - class DownloadBackupMailer < ActionMailer::Base include Email::BuildEmailHelper diff --git a/app/mailers/invite_mailer.rb b/app/mailers/invite_mailer.rb index 469a380415..050f6e8a7c 100644 --- a/app/mailers/invite_mailer.rb +++ b/app/mailers/invite_mailer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'email/message_builder' - class InviteMailer < ActionMailer::Base include Email::BuildEmailHelper diff --git a/app/mailers/rejection_mailer.rb b/app/mailers/rejection_mailer.rb index f91b98c4d8..99d6feb310 100644 --- a/app/mailers/rejection_mailer.rb +++ b/app/mailers/rejection_mailer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_dependency 'email/message_builder' +require 'email/message_builder' class RejectionMailer < ActionMailer::Base include Email::BuildEmailHelper diff --git a/app/mailers/subscription_mailer.rb b/app/mailers/subscription_mailer.rb index 0b13cb6a51..535dec5387 100644 --- a/app/mailers/subscription_mailer.rb +++ b/app/mailers/subscription_mailer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'email/message_builder' - class SubscriptionMailer < ActionMailer::Base include Email::BuildEmailHelper diff --git a/app/mailers/test_mailer.rb b/app/mailers/test_mailer.rb index a9eba9e9e5..ab65d300e8 100644 --- a/app/mailers/test_mailer.rb +++ b/app/mailers/test_mailer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'email/message_builder' - class TestMailer < ActionMailer::Base include Email::BuildEmailHelper diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index 0ffc6f77ba..28e154f8de 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -1,12 +1,5 @@ # frozen_string_literal: true -require_dependency 'markdown_linker' -require_dependency 'email/message_builder' -require_dependency 'age_words' -require_dependency 'rtl' -require_dependency 'discourse_ip_info' -require_dependency 'browser_detection' - class UserNotifications < ActionMailer::Base include UserNotificationsHelper include ApplicationHelper @@ -596,7 +589,7 @@ class UserNotifications < ActionMailer::Base end unless translation_override_exists - html = UserNotificationRenderer.with_view_paths(Rails.configuration.paths["app/views"]).render( + html = UserNotificationRenderer.render( template: 'email/notification', format: :html, locals: { context_posts: context_posts, diff --git a/app/mailers/version_mailer.rb b/app/mailers/version_mailer.rb index 4c12dffc17..0b9445f452 100644 --- a/app/mailers/version_mailer.rb +++ b/app/mailers/version_mailer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'email/message_builder' - class VersionMailer < ActionMailer::Base include Email::BuildEmailHelper diff --git a/app/models/about.rb b/app/models/about.rb index b24a62cd0f..df2d3ac3d4 100644 --- a/app/models/about.rb +++ b/app/models/about.rb @@ -81,18 +81,31 @@ class About end def category_moderators - category_ids = Guardian.new(@user).allowed_category_ids + allowed_cats = Guardian.new(@user).allowed_category_ids + return [] if allowed_cats.blank? + cats_with_mods = Category.where.not(reviewable_by_group_id: nil).pluck(:id) + category_ids = cats_with_mods & allowed_cats return [] if category_ids.blank? - results = DB.query(<<~SQL, category_ids: category_ids) - SELECT c.id category_id, array_agg(gu.user_id) user_ids + + per_cat_limit = category_mods_limit / category_ids.size + per_cat_limit = 1 if per_cat_limit < 1 + results = DB.query(<<~SQL, category_ids: category_ids, per_cat_limit: per_cat_limit) + SELECT c.id category_id, user_ids FROM categories c - JOIN group_users gu - ON gu.group_id = reviewable_by_group_id + CROSS JOIN LATERAL ( + SELECT ARRAY( + SELECT u.id + FROM users u + JOIN group_users gu + ON gu.group_id = c.reviewable_by_group_id AND gu.user_id = u.id + ORDER BY last_seen_at DESC + LIMIT :per_cat_limit + ) AS user_ids + ) user_ids WHERE c.id IN (:category_ids) - GROUP BY c.id SQL moderators = {} - User.where(id: results.map(&:user_ids).flatten).each do |user| + User.where(id: results.map(&:user_ids).flatten.uniq).each do |user| moderators[user.id] = user end moderators @@ -100,4 +113,12 @@ class About CategoryMods.new(row.category_id, row.user_ids.map { |id| moderators[id] }) end end + + def category_mods_limit + @category_mods_limit || 100 + end + + def category_mods_limit=(number) + @category_mods_limit = number + end end diff --git a/app/models/admin_dashboard_data.rb b/app/models/admin_dashboard_data.rb index c558954c3d..633b1a0019 100644 --- a/app/models/admin_dashboard_data.rb +++ b/app/models/admin_dashboard_data.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'mem_info' - class AdminDashboardData include StatsCacheable diff --git a/app/models/auto_track_duration_site_setting.rb b/app/models/auto_track_duration_site_setting.rb index 3659d81ce6..9764d0d525 100644 --- a/app/models/auto_track_duration_site_setting.rb +++ b/app/models/auto_track_duration_site_setting.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum_site_setting' - class AutoTrackDurationSiteSetting < EnumSiteSetting def self.valid_value?(val) diff --git a/app/models/backup_location_site_setting.rb b/app/models/backup_location_site_setting.rb index 4763c3be04..4236ca1de7 100644 --- a/app/models/backup_location_site_setting.rb +++ b/app/models/backup_location_site_setting.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum_site_setting' - class BackupLocationSiteSetting < EnumSiteSetting LOCAL ||= "local" S3 ||= "s3" diff --git a/app/models/badge.rb b/app/models/badge.rb index 561b562583..3596190a8a 100644 --- a/app/models/badge.rb +++ b/app/models/badge.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'slug' - class Badge < ActiveRecord::Base # NOTE: These badge ids are not in order! They are grouped logically. # When picking an id, *search* for it. diff --git a/app/models/category.rb b/app/models/category.rb index af7b073166..a7d2fe10a1 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'distributed_cache' - class Category < ActiveRecord::Base self.ignored_columns = %w{ uploaded_meta_id @@ -266,6 +264,15 @@ class Category < ActiveRecord::Base end end + def access_category_via_group + Group + .joins(:category_groups) + .where("category_groups.category_id = ?", self.id) + .where("groups.public_admission OR groups.allow_membership_requests") + .order(:allow_membership_requests) + .first + end + def duplicate_slug? Category.where(slug: self.slug, parent_category_id: parent_category_id).where.not(id: id).any? end diff --git a/app/models/category_list.rb b/app/models/category_list.rb index 2d8bfb3d74..f1f7b6a0d9 100644 --- a/app/models/category_list.rb +++ b/app/models/category_list.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'pinned_check' - class CategoryList include ActiveModel::Serialization diff --git a/app/models/category_user.rb b/app/models/category_user.rb index acf29cbefc..e25f917d32 100644 --- a/app/models/category_user.rb +++ b/app/models/category_user.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'notification_levels' - class CategoryUser < ActiveRecord::Base belongs_to :category belongs_to :user diff --git a/app/models/color_scheme.rb b/app/models/color_scheme.rb index c242b63442..f69a96031c 100644 --- a/app/models/color_scheme.rb +++ b/app/models/color_scheme.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'distributed_cache' - class ColorScheme < ActiveRecord::Base # rubocop:disable Layout/AlignHash diff --git a/app/models/concerns/second_factor_manager.rb b/app/models/concerns/second_factor_manager.rb index 67caf8adf0..7c20cc7ce2 100644 --- a/app/models/concerns/second_factor_manager.rb +++ b/app/models/concerns/second_factor_manager.rb @@ -51,6 +51,12 @@ module SecondFactorManager self&.user_second_factors.backup_codes.exists? end + def security_keys_enabled? + !SiteSetting.enable_sso && + SiteSetting.enable_local_logins && + self&.security_keys.where(factor_type: UserSecurityKey.factor_types[:second_factor], enabled: true).exists? + end + def remaining_backup_codes self&.user_second_factors&.backup_codes&.count end diff --git a/app/models/developer.rb b/app/models/developer.rb index db0958303a..0b0dcd9320 100644 --- a/app/models/developer.rb +++ b/app/models/developer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'distributed_cache' - class Developer < ActiveRecord::Base belongs_to :user diff --git a/app/models/digest_email_site_setting.rb b/app/models/digest_email_site_setting.rb index d0cc1be913..23cc4e28ec 100644 --- a/app/models/digest_email_site_setting.rb +++ b/app/models/digest_email_site_setting.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum_site_setting' - class DigestEmailSiteSetting < EnumSiteSetting def self.valid_value?(val) diff --git a/app/models/discourse_single_sign_on.rb b/app/models/discourse_single_sign_on.rb index ed6d73013e..6b6e392683 100644 --- a/app/models/discourse_single_sign_on.rb +++ b/app/models/discourse_single_sign_on.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'single_sign_on' - class DiscourseSingleSignOn < SingleSignOn class BlankExternalId < StandardError; end diff --git a/app/models/email_change_request.rb b/app/models/email_change_request.rb index b9f9e7bfeb..f54df4bf23 100644 --- a/app/models/email_change_request.rb +++ b/app/models/email_change_request.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'email_validator' - class EmailChangeRequest < ActiveRecord::Base belongs_to :old_email_token, class_name: 'EmailToken' belongs_to :new_email_token, class_name: 'EmailToken' diff --git a/app/models/email_level_site_setting.rb b/app/models/email_level_site_setting.rb index a3eccdf11c..9f805dd253 100644 --- a/app/models/email_level_site_setting.rb +++ b/app/models/email_level_site_setting.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum_site_setting' - class EmailLevelSiteSetting < EnumSiteSetting def self.valid_value?(val) diff --git a/app/models/email_log.rb b/app/models/email_log.rb index bc7df7ae2a..4ca15594f1 100644 --- a/app/models/email_log.rb +++ b/app/models/email_log.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'distributed_mutex' - class EmailLog < ActiveRecord::Base CRITICAL_EMAIL_TYPES ||= Set.new %w{ account_created diff --git a/app/models/embeddable_host.rb b/app/models/embeddable_host.rb index 788fc8b4d9..8944a4fcd7 100644 --- a/app/models/embeddable_host.rb +++ b/app/models/embeddable_host.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'url_helper' - class EmbeddableHost < ActiveRecord::Base validate :host_must_be_valid belongs_to :category diff --git a/app/models/group.rb b/app/models/group.rb index c3f632acd1..a773593107 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum' - class Group < ActiveRecord::Base include HasCustomFields include AnonCacheInvalidator diff --git a/app/models/group_user.rb b/app/models/group_user.rb index 456cbfbc42..664a333e9b 100644 --- a/app/models/group_user.rb +++ b/app/models/group_user.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'notification_levels' - class GroupUser < ActiveRecord::Base belongs_to :group, counter_cache: "user_count" belongs_to :user diff --git a/app/models/invite.rb b/app/models/invite.rb index e92d5cd408..34ff75b282 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'rate_limiter' - class Invite < ActiveRecord::Base self.ignored_columns = %w{ via_email diff --git a/app/models/like_notification_frequency_site_setting.rb b/app/models/like_notification_frequency_site_setting.rb index 80cdf1c882..19d7043627 100644 --- a/app/models/like_notification_frequency_site_setting.rb +++ b/app/models/like_notification_frequency_site_setting.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum_site_setting' - class LikeNotificationFrequencySiteSetting < EnumSiteSetting def self.valid_value?(val) diff --git a/app/models/locale_site_setting.rb b/app/models/locale_site_setting.rb index 4e5522a478..947fe5724a 100644 --- a/app/models/locale_site_setting.rb +++ b/app/models/locale_site_setting.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum_site_setting' - class LocaleSiteSetting < EnumSiteSetting def self.valid_value?(val) diff --git a/app/models/mailing_list_mode_site_setting.rb b/app/models/mailing_list_mode_site_setting.rb index 3d7d4881bc..2192a6fbf7 100644 --- a/app/models/mailing_list_mode_site_setting.rb +++ b/app/models/mailing_list_mode_site_setting.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum_site_setting' - class MailingListModeSiteSetting < EnumSiteSetting def self.valid_value?(val) val.to_i.to_s == val.to_s && diff --git a/app/models/new_topic_duration_site_setting.rb b/app/models/new_topic_duration_site_setting.rb index e2a944f877..3ed58210ae 100644 --- a/app/models/new_topic_duration_site_setting.rb +++ b/app/models/new_topic_duration_site_setting.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum_site_setting' - class NewTopicDurationSiteSetting < EnumSiteSetting def self.valid_value?(val) diff --git a/app/models/notification.rb b/app/models/notification.rb index b31812c75a..9b32cae7c0 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum' -require_dependency 'notification_emailer' - class Notification < ActiveRecord::Base belongs_to :user belongs_to :topic diff --git a/app/models/notification_level_when_replying_site_setting.rb b/app/models/notification_level_when_replying_site_setting.rb index 97e444a984..c71927815f 100644 --- a/app/models/notification_level_when_replying_site_setting.rb +++ b/app/models/notification_level_when_replying_site_setting.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum_site_setting' -require_dependency 'notification_levels' - class NotificationLevelWhenReplyingSiteSetting < EnumSiteSetting def self.valid_value?(val) diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb index 1f5b287f4d..cd50f70b58 100644 --- a/app/models/optimized_image.rb +++ b/app/models/optimized_image.rb @@ -1,10 +1,5 @@ # frozen_string_literal: true -require_dependency "file_helper" -require_dependency "url_helper" -require_dependency "db_helper" -require_dependency "file_store/local_store" - class OptimizedImage < ActiveRecord::Base include HasUrl belongs_to :upload diff --git a/app/models/post.rb b/app/models/post.rb index 2d2cb253e4..d2ad8be435 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -1,13 +1,5 @@ # frozen_string_literal: true -require_dependency 'pretty_text' -require_dependency 'rate_limiter' -require_dependency 'post_revisor' -require_dependency 'enum' -require_dependency 'post_analyzer' -require_dependency 'validators/post_validator' -require_dependency 'plugin/filter' - require 'archetype' require 'digest/sha1' @@ -55,7 +47,7 @@ class Post < ActiveRecord::Base has_many :user_actions, foreign_key: :target_post_id - validates_with ::Validators::PostValidator, unless: :skip_validation + validates_with PostValidator, unless: :skip_validation after_save :index_search diff --git a/app/models/post_action.rb b/app/models/post_action.rb index bf79a2a16e..5affd3b809 100644 --- a/app/models/post_action.rb +++ b/app/models/post_action.rb @@ -1,10 +1,5 @@ # frozen_string_literal: true -require_dependency 'rate_limiter' -require_dependency 'system_message' -require_dependency 'post_action_creator' -require_dependency 'post_action_destroyer' - class PostAction < ActiveRecord::Base include RateLimiter::OnCreateRecord include Trashable diff --git a/app/models/post_action_type.rb b/app/models/post_action_type.rb index a15ac36fd0..3a895f14d5 100644 --- a/app/models/post_action_type.rb +++ b/app/models/post_action_type.rb @@ -1,9 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum' -require_dependency 'distributed_cache' -require_dependency 'flag_settings' - class PostActionType < ActiveRecord::Base after_save :expire_cache after_destroy :expire_cache diff --git a/app/models/post_analyzer.rb b/app/models/post_analyzer.rb index cfc64e5308..1b5ca6970e 100644 --- a/app/models/post_analyzer.rb +++ b/app/models/post_analyzer.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require_dependency 'oneboxer' -require_dependency 'email_cook' - class PostAnalyzer def initialize(raw, topic_id) diff --git a/app/models/post_mover.rb b/app/models/post_mover.rb index 65cc60f6f9..6b779edf6b 100644 --- a/app/models/post_mover.rb +++ b/app/models/post_mover.rb @@ -64,6 +64,7 @@ class PostMover moving_all_posts = (@original_topic.posts.pluck(:id).sort == @post_ids.sort) create_temp_table + delete_invalid_post_timings move_each_post notify_users_that_posts_have_moved update_statistics @@ -76,11 +77,11 @@ class PostMover destination_topic.reload destination_topic - ensure - drop_temp_table end def create_temp_table + DB.exec("DROP TABLE IF EXISTS moved_posts") if Rails.env.test? + DB.exec <<~SQL CREATE TEMPORARY TABLE moved_posts ( old_topic_id INTEGER, @@ -90,17 +91,13 @@ class PostMover new_topic_title VARCHAR, new_post_id INTEGER, new_post_number INTEGER - ); + ) ON COMMIT DROP; CREATE INDEX moved_posts_old_post_number ON moved_posts(old_post_number); CREATE INDEX moved_posts_old_post_id ON moved_posts(old_post_id); SQL end - def drop_temp_table - DB.exec("DROP TABLE IF EXISTS moved_posts") - end - def move_each_post max_post_number = destination_topic.max_post_number + 1 @@ -290,6 +287,20 @@ class PostMover SQL end + def delete_invalid_post_timings + DB.exec(<<~SQL, topid_id: destination_topic.id) + DELETE + FROM post_timings pt + WHERE pt.topic_id = :topid_id + AND NOT EXISTS( + SELECT 1 + FROM posts p + WHERE p.topic_id = pt.topic_id + AND p.post_number = pt.post_number + ) + SQL + end + def move_post_timings DB.exec <<~SQL UPDATE post_timings pt diff --git a/app/models/post_revision.rb b/app/models/post_revision.rb index d4961eb865..4557f0f4da 100644 --- a/app/models/post_revision.rb +++ b/app/models/post_revision.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency "discourse_diff" - class PostRevision < ActiveRecord::Base belongs_to :post belongs_to :user diff --git a/app/models/post_timing.rb b/app/models/post_timing.rb index 9d7b2f1d30..96ee2f1782 100644 --- a/app/models/post_timing.rb +++ b/app/models/post_timing.rb @@ -1,6 +1,4 @@ # frozen_string_literal: true -# -require_dependency 'archetype' class PostTiming < ActiveRecord::Base belongs_to :topic @@ -73,6 +71,8 @@ class PostTiming < ActiveRecord::Base last_read_post_number: last_read ) + topic.posts.find_by(post_number: post_number).decrement!(:reads) + if !topic.private_message? set_minimum_first_unread!(user_id: user.id, date: topic.updated_at) end @@ -89,6 +89,8 @@ class PostTiming < ActiveRecord::Base .where('user_id = ? and topic_id in (?)', user_id, topic_ids) .delete_all + Post.where(topic_id: topic_ids).update_all('reads = reads - 1') + date = Topic.listable_topics.where(id: topic_ids).minimum(:updated_at) if date diff --git a/app/models/previous_replies_site_setting.rb b/app/models/previous_replies_site_setting.rb index 85f273cc70..59b7c2d246 100644 --- a/app/models/previous_replies_site_setting.rb +++ b/app/models/previous_replies_site_setting.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum_site_setting' - class PreviousRepliesSiteSetting < EnumSiteSetting def self.valid_value?(val) diff --git a/app/models/remote_theme.rb b/app/models/remote_theme.rb index 16b58579ca..c2cd50fbcf 100644 --- a/app/models/remote_theme.rb +++ b/app/models/remote_theme.rb @@ -1,9 +1,5 @@ # frozen_string_literal: true -require_dependency 'theme_store/git_importer' -require_dependency 'theme_store/tgz_importer' -require_dependency 'upload_creator' - class RemoteTheme < ActiveRecord::Base METADATA_PROPERTIES = %i{ license_url @@ -34,8 +30,8 @@ class RemoteTheme < ActiveRecord::Base raise ImportError.new I18n.t("themes.import_error.about_json") end - def self.update_tgz_theme(filename, match_theme: false, user: Discourse.system_user, theme_id: nil) - importer = ThemeStore::TgzImporter.new(filename) + def self.update_zipped_theme(filename, original_filename, match_theme: false, user: Discourse.system_user, theme_id: nil) + importer = ThemeStore::ZipImporter.new(filename, original_filename) importer.import! theme_info = RemoteTheme.extract_theme_info(importer) diff --git a/app/models/remove_muted_tags_from_latest_site_setting.rb b/app/models/remove_muted_tags_from_latest_site_setting.rb index 5c23198e24..9ca328e328 100644 --- a/app/models/remove_muted_tags_from_latest_site_setting.rb +++ b/app/models/remove_muted_tags_from_latest_site_setting.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum_site_setting' - class RemoveMutedTagsFromLatestSiteSetting < EnumSiteSetting ALWAYS ||= "always" diff --git a/app/models/report.rb b/app/models/report.rb index d4387b0f61..5271e0bef3 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'topic_subtype' - class Report # Change this line each time report format change # and you want to ensure cache is reset diff --git a/app/models/reviewable.rb b/app/models/reviewable.rb index 6fa2eccb6c..750f99544a 100644 --- a/app/models/reviewable.rb +++ b/app/models/reviewable.rb @@ -1,12 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum' -require_dependency 'reviewable/actions' -require_dependency 'reviewable/conversation' -require_dependency 'reviewable/editable_fields' -require_dependency 'reviewable/perform_result' -require_dependency 'reviewable_serializer' - class Reviewable < ActiveRecord::Base class UpdateConflict < StandardError; end @@ -166,6 +159,7 @@ class Reviewable < ActiveRecord::Base type_bonus = PostActionType.where(id: reviewable_score_type).pluck(:score_bonus)[0] || 0 take_action_bonus = take_action ? 5.0 : 0.0 sub_total = (ReviewableScore.user_flag_score(user) + type_bonus + take_action_bonus) + user_accuracy_bonus = ReviewableScore.user_accuracy_bonus(user) # We can force a reviewable to hit the threshold, for example with queued posts if force_review && sub_total < Reviewable.min_score_for_priority @@ -177,6 +171,7 @@ class Reviewable < ActiveRecord::Base status: ReviewableScore.statuses[:pending], reviewable_score_type: reviewable_score_type, score: sub_total, + user_accuracy_bonus: user_accuracy_bonus, meta_topic_id: meta_topic_id, take_action_bonus: take_action_bonus, created_at: created_at || Time.zone.now @@ -498,6 +493,7 @@ class Reviewable < ActiveRecord::Base us.flags_disagreed, us.flags_ignored, rs.score, + rs.user_accuracy_bonus, rs.take_action_bonus, COALESCE(pat.score_bonus, 0.0) AS type_bonus FROM reviewable_scores AS rs diff --git a/app/models/reviewable_flagged_post.rb b/app/models/reviewable_flagged_post.rb index 43622b1476..340d63c8b5 100644 --- a/app/models/reviewable_flagged_post.rb +++ b/app/models/reviewable_flagged_post.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'reviewable' - class ReviewableFlaggedPost < Reviewable # Penalties are handled by the modal after the action is performed diff --git a/app/models/reviewable_priority_setting.rb b/app/models/reviewable_priority_setting.rb index eb89b18a5a..42afd49775 100644 --- a/app/models/reviewable_priority_setting.rb +++ b/app/models/reviewable_priority_setting.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum_site_setting' - class ReviewablePrioritySetting < EnumSiteSetting def self.valid_value?(val) diff --git a/app/models/reviewable_queued_post.rb b/app/models/reviewable_queued_post.rb index 8602a0fb72..9c99be4c3d 100644 --- a/app/models/reviewable_queued_post.rb +++ b/app/models/reviewable_queued_post.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require_dependency 'reviewable' -require_dependency 'user_destroyer' - class ReviewableQueuedPost < Reviewable after_create do diff --git a/app/models/reviewable_score.rb b/app/models/reviewable_score.rb index 823e8935ce..1fe2897297 100644 --- a/app/models/reviewable_score.rb +++ b/app/models/reviewable_score.rb @@ -49,7 +49,8 @@ class ReviewableScore < ActiveRecord::Base # 1.0 + trust_level + user_accuracy_bonus # (trust_level is 5 for staff) def self.user_flag_score(user) - 1.0 + (user.staff? ? 5.0 : user.trust_level.to_f) + user_accuracy_bonus(user) + score = 1.0 + (user.staff? ? 5.0 : user.trust_level.to_f) + user_accuracy_bonus(user) + score >= 0 ? score : 0 end # A user's accuracy bonus is: @@ -68,8 +69,23 @@ class ReviewableScore < ActiveRecord::Base total = (agreed + disagreed).to_f return 0.0 if total <= 5 + accuracy_axis = 0.7 - (agreed / total) * 5.0 + percent_correct = agreed / total + positive_accuracy = percent_correct >= accuracy_axis + + bottom = positive_accuracy ? accuracy_axis : 0.0 + top = positive_accuracy ? 1.0 : accuracy_axis + + absolute_distance = positive_accuracy ? + percent_correct - bottom : + top - percent_correct + + axis_distance_multiplier = 1.0 / (top - bottom) + positivity_multiplier = positive_accuracy ? 1.0 : -1.0 + + (absolute_distance * axis_distance_multiplier * positivity_multiplier * (Math.log(total, 4) * 5.0)) + .round(2) end def reviewable_conversation diff --git a/app/models/reviewable_sensitivity_setting.rb b/app/models/reviewable_sensitivity_setting.rb index 1135c03a0d..4207d14260 100644 --- a/app/models/reviewable_sensitivity_setting.rb +++ b/app/models/reviewable_sensitivity_setting.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum_site_setting' - class ReviewableSensitivitySetting < EnumSiteSetting def self.valid_value?(val) diff --git a/app/models/reviewable_user.rb b/app/models/reviewable_user.rb index 31b8776ef8..59e107105c 100644 --- a/app/models/reviewable_user.rb +++ b/app/models/reviewable_user.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'reviewable' - class ReviewableUser < Reviewable def self.create_for(user) diff --git a/app/models/s3_region_site_setting.rb b/app/models/s3_region_site_setting.rb index 5d892d3192..0f6e03a208 100644 --- a/app/models/s3_region_site_setting.rb +++ b/app/models/s3_region_site_setting.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum_site_setting' - class S3RegionSiteSetting < EnumSiteSetting def self.valid_value?(val) diff --git a/app/models/screened_email.rb b/app/models/screened_email.rb index 316d898fba..73119db3b2 100644 --- a/app/models/screened_email.rb +++ b/app/models/screened_email.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'screening_model' - # A ScreenedEmail record represents an email address that is being watched, # typically when creating a new User account. If the email of the signup form # (or some other form) matches a ScreenedEmail record, an action can be diff --git a/app/models/screened_ip_address.rb b/app/models/screened_ip_address.rb index e4d89e5118..10ce1ce4f1 100644 --- a/app/models/screened_ip_address.rb +++ b/app/models/screened_ip_address.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require_dependency 'screening_model' -require_dependency 'ip_addr' +require 'screening_model' +require 'ip_addr' # A ScreenedIpAddress record represents an IP address or subnet that is being watched, # and possibly blocked from creating accounts. diff --git a/app/models/screened_url.rb b/app/models/screened_url.rb index a5bbaec497..0140dd2b47 100644 --- a/app/models/screened_url.rb +++ b/app/models/screened_url.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'screening_model' - # A ScreenedUrl record represents a URL that is being watched. # If the URL is found in a post, some action can be performed. diff --git a/app/models/search_log.rb b/app/models/search_log.rb index c628524238..a3899ff318 100644 --- a/app/models/search_log.rb +++ b/app/models/search_log.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum' - class SearchLog < ActiveRecord::Base validates_presence_of :term diff --git a/app/models/site.rb b/app/models/site.rb index efddc9cb4f..c7330e1e25 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true # A class we can use to serialize the site data -require_dependency 'score_calculator' -require_dependency 'trust_level' - class Site include ActiveModel::Serialization @@ -28,7 +25,7 @@ class Site end def user_fields - UserField.all + UserField.order(:position).all end def categories diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index 703dce9adb..bf82dbe8fa 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -1,9 +1,5 @@ # frozen_string_literal: true -require 'site_setting_extension' -require_dependency 'global_path' -require_dependency 'site_settings/yaml_loader' - class SiteSetting < ActiveRecord::Base extend GlobalPath extend SiteSettingExtension @@ -89,15 +85,6 @@ class SiteSetting < ActiveRecord::Base force_https? ? "https" : "http" end - def self.default_categories_selected - [ - SiteSetting.default_categories_watching.split("|"), - SiteSetting.default_categories_tracking.split("|"), - SiteSetting.default_categories_muted.split("|"), - SiteSetting.default_categories_watching_first_post.split("|") - ].flatten.to_set - end - def self.min_redirected_to_top_period(duration) ListController.best_period_with_topics_for(duration) end diff --git a/app/models/slug_setting.rb b/app/models/slug_setting.rb index 2feec4bd2a..65460f8390 100644 --- a/app/models/slug_setting.rb +++ b/app/models/slug_setting.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum_site_setting' - class SlugSetting < EnumSiteSetting VALUES = %w(ascii encoded none) diff --git a/app/models/tag_user.rb b/app/models/tag_user.rb index dc0164ebb1..820d750b66 100644 --- a/app/models/tag_user.rb +++ b/app/models/tag_user.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'notification_levels' - class TagUser < ActiveRecord::Base belongs_to :tag belongs_to :user diff --git a/app/models/theme.rb b/app/models/theme.rb index 613af2069d..aae30f6612 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -1,13 +1,5 @@ # frozen_string_literal: true -require_dependency 'distributed_cache' -require_dependency 'stylesheet/compiler' -require_dependency 'stylesheet/manager' -require_dependency 'theme_settings_parser' -require_dependency 'theme_settings_manager' -require_dependency 'theme_translation_parser' -require_dependency 'theme_translation_manager' - class Theme < ActiveRecord::Base @cache = DistributedCache.new('theme') diff --git a/app/models/theme_field.rb b/app/models/theme_field.rb index 8d8a0c6c99..ee94014585 100644 --- a/app/models/theme_field.rb +++ b/app/models/theme_field.rb @@ -1,9 +1,5 @@ # frozen_string_literal: true -require_dependency 'theme_settings_parser' -require_dependency 'theme_translation_parser' -require_dependency 'theme_javascript_compiler' - class ThemeField < ActiveRecord::Base belongs_to :upload @@ -64,7 +60,7 @@ class ThemeField < ActiveRecord::Base validates :name, format: { with: /\A[a-z_][a-z0-9_-]*\z/i }, if: Proc.new { |field| ThemeField.theme_var_type_ids.include?(field.type_id) } - BASE_COMPILER_VERSION = 12 + BASE_COMPILER_VERSION = 13 DEPENDENT_CONSTANTS = [BASE_COMPILER_VERSION, GlobalSetting.cdn_url] COMPILER_VERSION = Digest::SHA1.hexdigest(DEPENDENT_CONSTANTS.join) diff --git a/app/models/top_topic.rb b/app/models/top_topic.rb index 33933779c8..739f6ce66c 100644 --- a/app/models/top_topic.rb +++ b/app/models/top_topic.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency "distributed_memoizer" - class TopTopic < ActiveRecord::Base belongs_to :topic diff --git a/app/models/topic.rb b/app/models/topic.rb index b99b9c7015..ed98ba47f3 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -1,19 +1,5 @@ # frozen_string_literal: true -require_dependency 'slug' -require_dependency 'avatar_lookup' -require_dependency 'topic_view' -require_dependency 'rate_limiter' -require_dependency 'text_sentinel' -require_dependency 'text_cleaner' -require_dependency 'archetype' -require_dependency 'html_prettify' -require_dependency 'discourse_tagging' -require_dependency 'search_indexer' -require_dependency 'list_controller' -require_dependency 'topic_posters_summary' -require_dependency 'topic_featured_users' - class Topic < ActiveRecord::Base class UserExists < StandardError; end include ActionView::Helpers::SanitizeHelper @@ -47,6 +33,7 @@ class Topic < ActiveRecord::Base if deleted_at.nil? update_category_topic_count_by(-1) CategoryTagStat.topic_deleted(self) if self.tags.present? + DiscourseEvent.trigger(:topic_trashed, self) end super(trashed_by) self.topic_embed.trash! if has_topic_embed? @@ -56,6 +43,7 @@ class Topic < ActiveRecord::Base unless deleted_at.nil? update_category_topic_count_by(1) CategoryTagStat.topic_recovered(self) if self.tags.present? + DiscourseEvent.trigger(:topic_recovered, self) end # Note parens are required because superclass doesn't take `recovered_by` @@ -74,6 +62,7 @@ class Topic < ActiveRecord::Base presence: true, topic_title_length: true, censored_words: true, + watched_words: true, quality_title: { unless: :private_message? }, max_emojis: true, unique_among: { unless: Proc.new { |t| (SiteSetting.allow_duplicate_topic_titles? || t.private_message?) }, diff --git a/app/models/topic_embed.rb b/app/models/topic_embed.rb index 973b4896c5..895f0d5986 100644 --- a/app/models/topic_embed.rb +++ b/app/models/topic_embed.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require_dependency 'nokogiri' -require_dependency 'url_helper' - class TopicEmbed < ActiveRecord::Base include Trashable diff --git a/app/models/topic_link.rb b/app/models/topic_link.rb index cdd0cc4a2d..5222d438dc 100644 --- a/app/models/topic_link.rb +++ b/app/models/topic_link.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true require 'uri' -require_dependency 'slug' -require_dependency 'discourse' class TopicLink < ActiveRecord::Base diff --git a/app/models/topic_link_click.rb b/app/models/topic_link_click.rb index 452fb709f3..6b45f7e9af 100644 --- a/app/models/topic_link_click.rb +++ b/app/models/topic_link_click.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require_dependency 'discourse' require 'ipaddr' require 'url_helper' diff --git a/app/models/topic_list.rb b/app/models/topic_list.rb index e546df96f0..27730561d7 100644 --- a/app/models/topic_list.rb +++ b/app/models/topic_list.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require_dependency 'avatar_lookup' -require_dependency 'primary_group_lookup' - class TopicList include ActiveModel::Serialization diff --git a/app/models/topic_posters_summary.rb b/app/models/topic_posters_summary.rb index b054401617..a4299561e2 100644 --- a/app/models/topic_posters_summary.rb +++ b/app/models/topic_posters_summary.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true # This is used in topic lists -require_dependency 'topic_poster' - class TopicPostersSummary # localization is fast, but this allows us to avoid diff --git a/app/models/topic_tracking_state.rb b/app/models/topic_tracking_state.rb index cec4fa99e1..1b99db2095 100644 --- a/app/models/topic_tracking_state.rb +++ b/app/models/topic_tracking_state.rb @@ -390,6 +390,7 @@ SQL end def self.trigger_post_read_count_update(post, groups, last_read_post_number, user_id) + return if !post return if groups.empty? opts = { readers_count: post.readers_count, reader_id: user_id } post.publish_change_to_clients!(:read, opts) diff --git a/app/models/topic_user.rb b/app/models/topic_user.rb index 5b83e66f5f..65df38666f 100644 --- a/app/models/topic_user.rb +++ b/app/models/topic_user.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'notification_levels' - class TopicUser < ActiveRecord::Base belongs_to :user belongs_to :topic diff --git a/app/models/trust_level_and_staff_setting.rb b/app/models/trust_level_and_staff_setting.rb index 2795de8266..ecab020b93 100644 --- a/app/models/trust_level_and_staff_setting.rb +++ b/app/models/trust_level_and_staff_setting.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum_site_setting' - class TrustLevelAndStaffSetting < TrustLevelSetting def self.valid_value?(val) special_group?(val) || diff --git a/app/models/trust_level_setting.rb b/app/models/trust_level_setting.rb index a7d887c747..4660adc580 100644 --- a/app/models/trust_level_setting.rb +++ b/app/models/trust_level_setting.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum_site_setting' - class TrustLevelSetting < EnumSiteSetting def self.valid_value?(val) diff --git a/app/models/upload.rb b/app/models/upload.rb index b013ece4b4..5821afea84 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -1,12 +1,6 @@ # frozen_string_literal: true require "digest/sha1" -require_dependency "file_helper" -require_dependency "url_helper" -require_dependency "db_helper" -require_dependency "validators/upload_validator" -require_dependency "file_store/local_store" -require_dependency "base62" class Upload < ActiveRecord::Base include ActionView::Helpers::NumberHelper @@ -34,7 +28,7 @@ class Upload < ActiveRecord::Base validates_presence_of :filesize validates_presence_of :original_filename - validates_with ::Validators::UploadValidator + validates_with UploadValidator after_destroy do User.where(uploaded_avatar_id: self.id).update_all(uploaded_avatar_id: nil) diff --git a/app/models/user.rb b/app/models/user.rb index c1a94949a6..04c6af225a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,21 +1,5 @@ # frozen_string_literal: true -require_dependency 'jobs/base' -require_dependency 'email' -require_dependency 'email_token' -require_dependency 'email_validator' -require_dependency 'trust_level' -require_dependency 'pbkdf2' -require_dependency 'discourse' -require_dependency 'post_destroyer' -require_dependency 'user_name_suggester' -require_dependency 'pretty_text' -require_dependency 'url_helper' -require_dependency 'letter_avatar' -require_dependency 'promotion' -require_dependency 'password_validator' -require_dependency 'notification_serializer' - class User < ActiveRecord::Base include Searchable include Roleable @@ -79,6 +63,10 @@ class User < ActiveRecord::Base where(method: UserSecondFactor.methods[:totp], enabled: true) }, class_name: "UserSecondFactor" + has_many :security_keys, -> { + where(enabled: true) + }, class_name: "UserSecurityKey" + has_one :anonymous_user_master, class_name: 'AnonymousUser' has_one :anonymous_user_shadow, ->(record) { where(active: true) }, foreign_key: :master_user_id, class_name: 'AnonymousUser' @@ -1263,6 +1251,20 @@ class User < ActiveRecord::Base SQL end + def create_or_fetch_secure_identifier + return secure_identifier if secure_identifier.present? + new_secure_identifier = SecureRandom.hex(20) + self.update(secure_identifier: new_secure_identifier) + new_secure_identifier + end + + def second_factor_security_key_credential_ids + security_keys + .select(:credential_id) + .where(factor_type: UserSecurityKey.factor_types[:second_factor]) + .pluck(:credential_id) + end + protected def badge_grant diff --git a/app/models/user_avatar.rb b/app/models/user_avatar.rb index 741f952aca..215524a743 100644 --- a/app/models/user_avatar.rb +++ b/app/models/user_avatar.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require_dependency 'letter_avatar' -require_dependency 'upload_creator' - class UserAvatar < ActiveRecord::Base belongs_to :user belongs_to :gravatar_upload, class_name: 'Upload' diff --git a/app/models/user_email.rb b/app/models/user_email.rb index 8b3729f1fb..e6fdaa8fc6 100644 --- a/app/models/user_email.rb +++ b/app/models/user_email.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'email_validator' - class UserEmail < ActiveRecord::Base belongs_to :user diff --git a/app/models/user_profile.rb b/app/models/user_profile.rb index 3e51fd9173..7f16cfe90e 100644 --- a/app/models/user_profile.rb +++ b/app/models/user_profile.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require_dependency 'upload_creator' class UserProfile < ActiveRecord::Base self.ignored_columns = %w{ card_background diff --git a/app/models/user_search.rb b/app/models/user_search.rb index 86ae6d5d75..ffb23e46f9 100644 --- a/app/models/user_search.rb +++ b/app/models/user_search.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true # Searches for a user by username or full text or name (if enabled in SiteSettings) -require_dependency 'search' - class UserSearch MAX_SIZE_PRIORITY_MENTION ||= 500 diff --git a/app/models/user_security_key.rb b/app/models/user_security_key.rb new file mode 100644 index 0000000000..61f325bd0d --- /dev/null +++ b/app/models/user_security_key.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class UserSecurityKey < ActiveRecord::Base + belongs_to :user + + scope :second_factors, -> do + where(factor_type: UserSecurityKey.factor_types[:second_factor], enabled: true) + end + + def self.factor_types + @factor_types ||= Enum.new( + second_factor: 0, + first_factor: 1, + multi_factor: 2, + ) + end +end + +# == Schema Information +# +# Table name: user_security_keys +# +# id :bigint not null, primary key +# user_id :integer not null +# factor_type :integer not null +# credential_id :string not null, UNIQUE +# public_key :string not null +# enabled :boolean default(FALSE), not null +# last_used :datetime +# created_at :datetime not null +# updated_at :datetime not null +# name :string not null +# +# Indexes +# +# index_user_security_keys_on_credential_id (credential_id) (UNIQUE) +# index_user_security_keys_on_factor_type (factor_type) +# diff --git a/app/models/username_validator.rb b/app/models/username_validator.rb index 1f2e9b9799..8a588ad56c 100644 --- a/app/models/username_validator.rb +++ b/app/models/username_validator.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'user' - class UsernameValidator # Public: Perform the validation of a field in a given object # it adds the errors (if any) to the object that we're giving as parameter diff --git a/app/models/watched_word.rb b/app/models/watched_word.rb index 959dfb011d..57cc69f1fc 100644 --- a/app/models/watched_word.rb +++ b/app/models/watched_word.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'enum' - class WatchedWord < ActiveRecord::Base def self.actions diff --git a/app/serializers/admin_user_serializer.rb b/app/serializers/admin_user_serializer.rb index fed45ea732..e864578f91 100644 --- a/app/serializers/admin_user_serializer.rb +++ b/app/serializers/admin_user_serializer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'admin_user_list_serializer' - class AdminUserSerializer < AdminUserListSerializer attributes :name, diff --git a/app/serializers/api_key_serializer.rb b/app/serializers/api_key_serializer.rb index bb807269b8..59f874be3e 100644 --- a/app/serializers/api_key_serializer.rb +++ b/app/serializers/api_key_serializer.rb @@ -3,7 +3,9 @@ class ApiKeySerializer < ApplicationSerializer attributes :id, - :key + :key, + :last_used_at, + :created_at has_one :user, serializer: BasicUserSerializer, embed: :objects diff --git a/app/serializers/auth_provider_serializer.rb b/app/serializers/auth_provider_serializer.rb index 404e3d9425..320a2654cf 100644 --- a/app/serializers/auth_provider_serializer.rb +++ b/app/serializers/auth_provider_serializer.rb @@ -3,7 +3,7 @@ class AuthProviderSerializer < ApplicationSerializer attributes :name, :custom_url, :pretty_name_override, :title_override, :message_override, - :frame_width, :frame_height, :full_screen_login, :can_connect, :can_revoke, + :frame_width, :frame_height, :can_connect, :can_revoke, :icon def title_override @@ -16,12 +16,6 @@ class AuthProviderSerializer < ApplicationSerializer object.pretty_name end - def full_screen_login - return SiteSetting.get(object.full_screen_login_setting) if object.full_screen_login_setting - return object.full_screen_login if object.full_screen_login - false - end - def message_override object.message end diff --git a/app/serializers/concerns/user_auth_tokens_mixin.rb b/app/serializers/concerns/user_auth_tokens_mixin.rb index 5b9ae19673..8e963f7fad 100644 --- a/app/serializers/concerns/user_auth_tokens_mixin.rb +++ b/app/serializers/concerns/user_auth_tokens_mixin.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require_dependency 'browser_detection' -require_dependency 'discourse_ip_info' - module UserAuthTokensMixin extend ActiveSupport::Concern diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index 8cfb348e62..4ba1a982f8 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'new_post_manager' - class CurrentUserSerializer < BasicUserSerializer attributes :name, diff --git a/app/serializers/hidden_topic_view_serializer.rb b/app/serializers/hidden_topic_view_serializer.rb deleted file mode 100644 index 414299f7e7..0000000000 --- a/app/serializers/hidden_topic_view_serializer.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -class HiddenTopicViewSerializer < ApplicationSerializer - attributes :view_hidden? - - has_one :group, serializer: BasicGroupSerializer, root: false, embed: :objects - - def view_hidden? - true - end - - def group - object.access_topic_via_group - end -end diff --git a/app/serializers/listable_topic_serializer.rb b/app/serializers/listable_topic_serializer.rb index 2b110290e8..1336a92c14 100644 --- a/app/serializers/listable_topic_serializer.rb +++ b/app/serializers/listable_topic_serializer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'pinned_check' - class ListableTopicSerializer < BasicTopicSerializer attributes :reply_count, diff --git a/app/serializers/new_post_result_serializer.rb b/app/serializers/new_post_result_serializer.rb index 3031348da7..ca6eee7f9c 100644 --- a/app/serializers/new_post_result_serializer.rb +++ b/app/serializers/new_post_result_serializer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'application_serializer' - class NewPostResultSerializer < ApplicationSerializer attributes :action, :post, diff --git a/app/serializers/post_action_type_serializer.rb b/app/serializers/post_action_type_serializer.rb index f6aec43499..7c492cb1a3 100644 --- a/app/serializers/post_action_type_serializer.rb +++ b/app/serializers/post_action_type_serializer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'configurable_urls' - class PostActionTypeSerializer < ApplicationSerializer attributes( diff --git a/app/serializers/post_stream_serializer_mixin.rb b/app/serializers/post_stream_serializer_mixin.rb index 794b1bffb4..9b435155d9 100644 --- a/app/serializers/post_stream_serializer_mixin.rb +++ b/app/serializers/post_stream_serializer_mixin.rb @@ -1,9 +1,5 @@ # frozen_string_literal: true -require_dependency 'gap_serializer' -require_dependency 'post_serializer' -require_dependency 'timeline_lookup' - module PostStreamSerializerMixin def self.included(klass) klass.attributes :post_stream diff --git a/app/serializers/reviewable_explanation_serializer.rb b/app/serializers/reviewable_explanation_serializer.rb index 057c4dc3ce..f268d12794 100644 --- a/app/serializers/reviewable_explanation_serializer.rb +++ b/app/serializers/reviewable_explanation_serializer.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require_dependency 'reviewable_score_explanation_serializer' class ReviewableExplanationSerializer < ApplicationSerializer attributes( diff --git a/app/serializers/reviewable_score_explanation_serializer.rb b/app/serializers/reviewable_score_explanation_serializer.rb index 73dccc01bf..f008202288 100644 --- a/app/serializers/reviewable_score_explanation_serializer.rb +++ b/app/serializers/reviewable_score_explanation_serializer.rb @@ -12,9 +12,4 @@ class ReviewableScoreExplanationSerializer < ApplicationSerializer :user_accuracy_bonus, :score ) - - def user_accuracy_bonus - ReviewableScore.calc_user_accuracy_bonus(object.flags_agreed, object.flags_disagreed) - end - end diff --git a/app/serializers/reviewable_score_serializer.rb b/app/serializers/reviewable_score_serializer.rb index 8bcf089098..4232c0c25c 100644 --- a/app/serializers/reviewable_score_serializer.rb +++ b/app/serializers/reviewable_score_serializer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'reviewable_score_type_serializer' - class ReviewableScoreSerializer < ApplicationSerializer attributes :id, :score, :agree_stats, :status, :reason, :created_at, :reviewed_at diff --git a/app/serializers/reviewable_serializer.rb b/app/serializers/reviewable_serializer.rb index d1a99c8a20..b2cbdab459 100644 --- a/app/serializers/reviewable_serializer.rb +++ b/app/serializers/reviewable_serializer.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require_dependency 'reviewable_action_serializer' -require_dependency 'reviewable_editable_field_serializer' - class ReviewableSerializer < ApplicationSerializer class_attribute :_payload_for_serialization diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb index 6e579d7719..3f8a653d96 100644 --- a/app/serializers/site_serializer.rb +++ b/app/serializers/site_serializer.rb @@ -1,9 +1,5 @@ # frozen_string_literal: true -require_dependency 'discourse_tagging' -require_dependency 'wizard' -require_dependency 'wizard/builder' - class SiteSerializer < ApplicationSerializer attributes( diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb index 1f17764516..233303f8f0 100644 --- a/app/serializers/topic_view_serializer.rb +++ b/app/serializers/topic_view_serializer.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require_dependency 'pinned_check' -require_dependency 'new_post_manager' - class TopicViewSerializer < ApplicationSerializer include PostStreamSerializerMixin include SuggestedTopicsMixin diff --git a/app/serializers/trust_level3_requirements_serializer.rb b/app/serializers/trust_level3_requirements_serializer.rb index 6f5a726830..2cf8331759 100644 --- a/app/serializers/trust_level3_requirements_serializer.rb +++ b/app/serializers/trust_level3_requirements_serializer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'penalty_counts_serializer' - class TrustLevel3RequirementsSerializer < ApplicationSerializer has_one :penalty_counts, embed: :object, serializer: PenaltyCountsSerializer diff --git a/app/serializers/web_hook_post_serializer.rb b/app/serializers/web_hook_post_serializer.rb index 2f78f3ca88..030b55203b 100644 --- a/app/serializers/web_hook_post_serializer.rb +++ b/app/serializers/web_hook_post_serializer.rb @@ -2,7 +2,9 @@ class WebHookPostSerializer < PostSerializer - attributes :topic_posts_count + attributes :topic_posts_count, + :topic_archetype, + :category_slug def include_topic_title? true @@ -32,6 +34,18 @@ class WebHookPostSerializer < PostSerializer object.topic ? object.topic.posts_count : 0 end + def topic_archetype + object.topic ? object.topic.archetype : '' + end + + def include_category_slug? + object.topic && object.topic.category + end + + def category_slug + object.topic && object.topic.category ? object.topic.category.slug_for_url : '' + end + def include_readers_count? false end diff --git a/app/serializers/web_hook_topic_view_serializer.rb b/app/serializers/web_hook_topic_view_serializer.rb index 0d83cee5ae..90defd183f 100644 --- a/app/serializers/web_hook_topic_view_serializer.rb +++ b/app/serializers/web_hook_topic_view_serializer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'pinned_check' - class WebHookTopicViewSerializer < TopicViewSerializer attributes :created_by, :last_poster diff --git a/app/services/group_message.rb b/app/services/group_message.rb index 0bc9a9005c..5c22d90d52 100644 --- a/app/services/group_message.rb +++ b/app/services/group_message.rb @@ -10,10 +10,6 @@ # limit_once_per: (seconds) Limit sending the given type of message once every X seconds. # The default is 24 hours. Set to false to always send the message. -require_dependency 'post_creator' -require_dependency 'topic_subtype' -require_dependency 'discourse' - class GroupMessage include Rails.application.routes.url_helpers diff --git a/app/services/inline_uploads.rb b/app/services/inline_uploads.rb index 66fead7921..8168543b24 100644 --- a/app/services/inline_uploads.rb +++ b/app/services/inline_uploads.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency "pretty_text" - class InlineUploads PLACEHOLDER = "__replace__" PATH_PLACEHOLDER = "__replace_path__" diff --git a/app/services/post_alerter.rb b/app/services/post_alerter.rb index d48fc2a2dd..3fe1a8899a 100644 --- a/app/services/post_alerter.rb +++ b/app/services/post_alerter.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require_dependency 'distributed_mutex' -require_dependency 'user_action_manager' - class PostAlerter def self.post_created(post, opts = {}) PostAlerter.new(opts).after_save_post(post, true) diff --git a/app/services/post_owner_changer.rb b/app/services/post_owner_changer.rb index 45ed22688c..9a2d128c45 100644 --- a/app/services/post_owner_changer.rb +++ b/app/services/post_owner_changer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'post_action_destroyer' - class PostOwnerChanger def initialize(params) diff --git a/app/services/push_notification_pusher.rb b/app/services/push_notification_pusher.rb index 82d65196d6..93ae8a4e4e 100644 --- a/app/services/push_notification_pusher.rb +++ b/app/services/push_notification_pusher.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'webpush' - class PushNotificationPusher TOKEN_VALID_FOR_SECONDS ||= 5 * 60 diff --git a/app/services/search_indexer.rb b/app/services/search_indexer.rb index 4ea9b7b972..8b3c84944b 100644 --- a/app/services/search_indexer.rb +++ b/app/services/search_indexer.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require_dependency 'search' class SearchIndexer INDEX_VERSION = 3 diff --git a/app/services/staff_action_logger.rb b/app/services/staff_action_logger.rb index 765a05b542..e564c6c790 100644 --- a/app/services/staff_action_logger.rb +++ b/app/services/staff_action_logger.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'staff_message_format' - # Responsible for logging the actions of admins and moderators. class StaffActionLogger diff --git a/app/services/user_destroyer.rb b/app/services/user_destroyer.rb index dbdbc1daef..abb5b9b3ca 100644 --- a/app/services/user_destroyer.rb +++ b/app/services/user_destroyer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'ip_addr' - # Responsible for destroying a User record class UserDestroyer diff --git a/app/services/user_notification_renderer.rb b/app/services/user_notification_renderer.rb index 8ad7d20914..64bb497dcf 100644 --- a/app/services/user_notification_renderer.rb +++ b/app/services/user_notification_renderer.rb @@ -4,4 +4,16 @@ class UserNotificationRenderer < ActionView::Base include ApplicationHelper include UserNotificationsHelper include EmailHelper + + LOCK = Mutex.new + + def self.render(*args) + LOCK.synchronize do + @instance ||= UserNotificationRenderer.with_view_paths( + Rails.configuration.paths["app/views"] + ) + @instance.render(*args) + end + end + end diff --git a/app/services/user_silencer.rb b/app/services/user_silencer.rb index 014d65aa5d..6dd2edf736 100644 --- a/app/services/user_silencer.rb +++ b/app/services/user_silencer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'staff_message_format' - class UserSilencer attr_reader :user_history diff --git a/app/services/username_changer.rb b/app/services/username_changer.rb index 6b23d3ec5d..7c6cec61cc 100644 --- a/app/services/username_changer.rb +++ b/app/services/username_changer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'jobs/regular/update_username' - class UsernameChanger def initialize(user, new_username, actor = nil) diff --git a/app/views/common/_second_factor_form_script.html.erb b/app/views/common/_second_factor_form_script.html.erb deleted file mode 100644 index 226136b7be..0000000000 --- a/app/views/common/_second_factor_form_script.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -<%= javascript_tag do %> - var useTotp = "<%= t("login.second_factor_toggle.totp") %>"; - var useBackup = "<%= t("login.second_factor_toggle.backup_code") %>"; - var backupForm = document.getElementById("backup-second-factor-form"); - var primaryForm = document.getElementById("primary-second-factor-form"); - document.getElementById("toggle-form").onclick = function(event) { - event.preventDefault(); - if (backupForm.style.display === "none") { - backupForm.style.display = "block"; - primaryForm.style.display = "none"; - document.getElementById("toggle-form").innerHTML = useTotp; - } else { - backupForm.style.display = "none"; - primaryForm.style.display = "block"; - document.getElementById("toggle-form").innerHTML = useBackup; - } - } -<% end %> diff --git a/app/views/exceptions/not_found.html.erb b/app/views/exceptions/not_found.html.erb index a1173a8feb..805b1c96b9 100644 --- a/app/views/exceptions/not_found.html.erb +++ b/app/views/exceptions/not_found.html.erb @@ -1,4 +1,12 @@ -

<%= t 'page_not_found.title' %>

+
+

<%= @title %>

+ + <%- if @group&.allow_membership_requests %> + <%= SvgSprite.raw_svg('user-plus') %> <%= I18n.t('not_in_group.request_membership') %> + <%- elsif @group&.public_admission %> + <%= SvgSprite.raw_svg('user-plus') %> <%= I18n.t('not_in_group.join_group') %> + <%- end %> +
<%= build_plugin_html 'server:not-found-before-topics' %> diff --git a/app/views/users/admin_login.html.erb b/app/views/users/admin_login.html.erb index aa29bc0fa8..42d3a5e637 100644 --- a/app/views/users/admin_login.html.erb +++ b/app/views/users/admin_login.html.erb @@ -1,39 +1,61 @@ - - - Admin Login - - - <% if @message %> - <%= @message %> - <% if @error %>

<%= @error %>

<% end %> +<% if @message %> + <%= @message %> + <% if @error %>

<%= @error %>

<% end %> - <% if @second_factor_required %> -
- <%=form_tag({}, method: :put) do %> - <%= label_tag(:second_factor_token, t('login.second_factor_description')) %> - <%= render 'common/second_factor_text_field' %>

- <%= submit_tag t('submit')%> - <% end %> -
+ <% if @security_key_required %> +
+
+ <%= hidden_field_tag 'security_key_challenge', @security_key_challenge, id: 'security-key-challenge' %> + <%= hidden_field_tag 'security_key_allowed_credential_ids', @security_key_allowed_credential_ids, id: 'security-key-allowed-credential-ids' %> - <%if @backup_codes_enabled%> - - <%=t "login.second_factor_backup" %> - <%= render 'common/second_factor_form_script' %> - <%end%> - <% end %> - <% else %> <%=form_tag({}, method: :put) do %> - <%= label_tag(:email, t('admin_login.email_input')) %> - <%= text_field_tag(:email, nil, autofocus: true) %>

- <%= submit_tag t('admin_login.submit_button') %> +

<%= t('login.security_key_authenticate') %>

+

<%= t('login.security_key_description') %>

+ <%= hidden_field_tag 'second_factor_method', '3' %> + <%= hidden_field_tag 'security_key_credential', nil, id: 'security-key-credential' %> + + <% if @second_factor_required %> + <%= link_to t('login.security_key_alternative'), '#', id: 'activate-security-key-alternative' %>

+ <% end %> + <%= button_tag t('login.security_key_authenticate'), id: 'submit-security-key' %> <% end %> - <% end %> - - +
+ <% end %> + + <% if @second_factor_required %> +
+
+ <%=form_tag({}, method: :put) do %> +
+ <%= label_tag(:second_factor_token, t('login.second_factor_description')) %> + <%= render 'common/second_factor_text_field' %>

+ <%= submit_tag t('submit')%> + <% end %> +
+ + <%if @backup_codes_enabled%> + + <%=t "login.second_factor_backup" %> + <%end%> +
+ <% end %> +<% else %> + <%=form_tag({}, method: :put) do %> + <%= label_tag(:email, t('admin_login.email_input')) %> + <%= text_field_tag(:email, nil, autofocus: true) %>

+ <%= submit_tag t('admin_login.submit_button') %> + <% end %> +<% end %> + +<%= preload_script "ember_jquery" %> +<%= preload_script "locales/#{I18n.locale}" %> +<%= preload_script "locales/i18n" %> +<%= preload_script "discourse/lib/webauthn" %> +<%= preload_script "admin-login/admin-login" %> +<%= preload_script "admin-login/admin-login.no-module" %> diff --git a/app/views/users/omniauth_callbacks/complete.html.erb b/app/views/users/omniauth_callbacks/complete.html.erb deleted file mode 100644 index dc6a0b0d4d..0000000000 --- a/app/views/users/omniauth_callbacks/complete.html.erb +++ /dev/null @@ -1,33 +0,0 @@ - - - - <%= SiteSetting.title %> - - - - <%= tag.meta id: 'data-auth-result', data: { - auth_result: @auth_result.to_client_hash, - base_url: Discourse.base_url - } %> - <%= preload_script('omniauth-complete') %> - - - -
-

- <%=t "login.auth_complete" %> - <%= t("login.click_to_continue") %> -

-
- - diff --git a/app/views/users/omniauth_callbacks/confirm_request.html.erb b/app/views/users/omniauth_callbacks/confirm_request.html.erb index a25158dbb3..9fa44acc4d 100644 --- a/app/views/users/omniauth_callbacks/confirm_request.html.erb +++ b/app/views/users/omniauth_callbacks/confirm_request.html.erb @@ -3,7 +3,6 @@
<%= form_tag do %> - <%= button_tag(type: "submit", class: "btn btn-primary") do %> <%= SvgSprite.raw_svg('fa-plug') %><%= t 'login.omniauth_confirm_button' %> <% end %> diff --git a/bin/turbo_rspec b/bin/turbo_rspec index 035fae706a..66464bc67b 100755 --- a/bin/turbo_rspec +++ b/bin/turbo_rspec @@ -1,12 +1,15 @@ #!/usr/bin/env ruby # frozen_string_literal: true +ENV['RAILS_ENV'] ||= 'test' + require './lib/turbo_tests' require 'optparse' requires = [] formatters = [] verbose = false +fail_fast = nil OptionParser.new do |opts| opts.on("-r", "--require PATH", "Require a file.") do |filename| @@ -33,6 +36,12 @@ OptionParser.new do |opts| opts.on("-v", "--verbose", "More output") do verbose = true end + + opts.on("--fail-fast=[N]") do |n| + n = Integer(n) rescue nil + fail_fast = (n.nil? || n < 1) ? 1 : n + end + end.parse!(ARGV) requires.each { |f| require(f) } @@ -54,7 +63,8 @@ success = TurboTests::Runner.run( formatters: formatters, files: ARGV.empty? ? ["spec"] : ARGV, - verbose: verbose + verbose: verbose, + fail_fast: fail_fast ) if success diff --git a/config/application.rb b/config/application.rb index 932aed2806..dd56bf859f 100644 --- a/config/application.rb +++ b/config/application.rb @@ -21,6 +21,7 @@ require 'action_mailer/railtie' require 'sprockets/railtie' # Plugin related stuff +require_relative '../lib/plugin_initialization_guard' require_relative '../lib/discourse_event' require_relative '../lib/discourse_plugin' require_relative '../lib/discourse_plugin_registry' @@ -93,16 +94,20 @@ module Discourse # issue is image_optim crashes on missing dependencies config.assets.image_optim = false - config.autoloader = :classic + config.autoloader = :zeitwerk # Custom directories with classes and modules you want to be autoloadable. - config.autoload_paths += Dir["#{config.root}/app/serializers"] - config.autoload_paths += Dir["#{config.root}/lib/validators/"] config.autoload_paths += Dir["#{config.root}/app"] + config.autoload_paths += Dir["#{config.root}/app/jobs"] + config.autoload_paths += Dir["#{config.root}/app/serializers"] + config.autoload_paths += Dir["#{config.root}/lib"] + config.autoload_paths += Dir["#{config.root}/lib/active_record/connection_adapters"] + config.autoload_paths += Dir["#{config.root}/lib/common_passwords"] + config.autoload_paths += Dir["#{config.root}/lib/highlight_js"] + config.autoload_paths += Dir["#{config.root}/lib/i18n"] + config.autoload_paths += Dir["#{config.root}/lib/validators/"] - if Rails.env.development? && !Sidekiq.server? - config.autoload_paths += Dir["#{config.root}/lib"] - end + Rails.autoloaders.main.ignore(Dir["#{config.root}/app/models/reports"]) # Only load the plugins named here, in the order given (default is alphabetical). # :all can be used as a placeholder for all plugins not explicitly named. @@ -144,6 +149,10 @@ module Discourse activate-account.js auto-redirect.js wizard-start.js + locales/i18n.js + discourse/lib/webauthn.js + admin-login/admin-login.js + admin-login/admin-login.no-module.js onpopstate-handler.js embed-application.js } @@ -258,7 +267,9 @@ module Discourse Discourse.activate_plugins! end else - Discourse.activate_plugins! + plugin_initialization_guard do + Discourse.activate_plugins! + end end Discourse.find_plugin_js_assets(include_disabled: true).each do |file| @@ -293,7 +304,9 @@ module Discourse OpenID::Util.logger = Rails.logger # Load plugins - Discourse.plugins.each(&:notify_after_initialize) + plugin_initialization_guard do + Discourse.plugins.each(&:notify_after_initialize) + end # we got to clear the pool in case plugins connect ActiveRecord::Base.connection_handler.clear_active_connections! diff --git a/config/initializers/000-post_migration.rb b/config/initializers/000-post_migration.rb index ca0c06d8a8..c5dfc682cb 100644 --- a/config/initializers/000-post_migration.rb +++ b/config/initializers/000-post_migration.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true unless Discourse.skip_post_deployment_migrations? - Rails.application.config.paths['db/migrate'] << Rails.root.join( + ActiveRecord::Migrator.migrations_paths << Rails.root.join( Discourse::DB_POST_MIGRATE_PATH ).to_s end diff --git a/config/initializers/005-site_settings.rb b/config/initializers/005-site_settings.rb index 7f4470417f..4ecaa85df3 100644 --- a/config/initializers/005-site_settings.rb +++ b/config/initializers/005-site_settings.rb @@ -6,6 +6,10 @@ Discourse.git_version if GlobalSetting.skip_redis? + # Requiring this file explicitly prevents it from being autoloaded and so the + # provider attribute is not cleared + require File.expand_path('../../../app/models/site_setting', __FILE__) + require 'site_settings/local_process_provider' Rails.cache = Discourse.cache SiteSetting.provider = SiteSettings::LocalProcessProvider.new diff --git a/config/initializers/013-excon_defaults.rb b/config/initializers/013-excon_defaults.rb index cfade5b7cc..40389051db 100644 --- a/config/initializers/013-excon_defaults.rb +++ b/config/initializers/013-excon_defaults.rb @@ -1,3 +1,4 @@ # frozen_string_literal: true +require 'excon' Excon::DEFAULTS[:omit_default_port] = true diff --git a/config/initializers/014-track-setting-changes.rb b/config/initializers/014-track-setting-changes.rb index 00150af49e..e5ca134122 100644 --- a/config/initializers/014-track-setting-changes.rb +++ b/config/initializers/014-track-setting-changes.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency "site_icon_manager" - DiscourseEvent.on(:site_setting_changed) do |name, old_value, new_value| # Enabling `must_approve_users` on an existing site is odd, so we assume that the # existing users are approved. diff --git a/config/initializers/099-defer.rb b/config/initializers/099-defer.rb deleted file mode 100644 index 96f1456b64..0000000000 --- a/config/initializers/099-defer.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -require_dependency 'scheduler/defer' diff --git a/config/initializers/100-onebox_options.rb b/config/initializers/100-onebox_options.rb index c6bd15fad9..e9d0f21c71 100644 --- a/config/initializers/100-onebox_options.rb +++ b/config/initializers/100-onebox_options.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'twitter_api' - Onebox.options = { twitter_client: TwitterApi, redirect_limit: 3, diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index a5df8c084b..be1aa441a0 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -759,14 +759,16 @@ ar: copied_to_clipboard: "تم نسخه الى لوحة العمل" copy_to_clipboard_error: "خطأ في نسخ البيانات الى لوحة العمل" second_factor: + name: "الإسم" label: "كود" disable_description: "يرجى ادخال رمز التوثيق من التطبيق الخاص بك" show_key_description: "أضف يدويا" short_description: | قم بحماية الحساب الخاص بك مع رمز الامان ذو الاستخدام الواحد. oauth_enabled_warning: "يرجى الملاحظة ان تسجيل الدخول عن طريق حسابات مواقع التواصل الاجتماعي سيتم تعطيلها بمجرد تفعيل خاصية التوثيق بعاملين الحساب" - use: "استخدم تطبيق التوثيق" edit: "عدّل" + security_key: + delete: 'أحذف' change_about: title: "تعديل عني" error: "حدث عطل أثناء تغيير هذه القيمة." @@ -1151,20 +1153,15 @@ ar: google_oauth2: name: "غوغل" title: "عبر Google " - message: "تسجيل الدخول عبر Google (تأكد من السماح للـ pop up)" twitter: name: "Twitter" title: "عبر Twitter" - message: "تسجيل الدخول عبر Twitter (تأكد من السماح للـ pop up)" instagram: title: "عبر Instagram" - message: "تسجيل الدخول عبر Instagram (تأكد من السماح للـ pop up)" facebook: title: "عبر Facebook" - message: "تسجيل الدخول عبر Facebook (تأكد من السماح للـ pop up)" github: title: "عبر GitHub" - message: "تسجيل الدخول عبر GitHub (تأكد من السماح للـ pop up)" invites: accept_title: "دعوة" welcome_to: "مرحباً بك في %{site_name}!" @@ -1182,7 +1179,6 @@ ar: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -2648,6 +2644,7 @@ ar: user: "الاعضاء" title: "API" key: "مفتاح API" + created: أُنشئ في generate: "إنشاء" regenerate: "إعادة إنشاء" revoke: "الغاء" diff --git a/config/locales/client.be.yml b/config/locales/client.be.yml index bac4093016..8e858e2f1b 100644 --- a/config/locales/client.be.yml +++ b/config/locales/client.be.yml @@ -439,7 +439,11 @@ be: enable: "Уключыць" second_factor: title: "Два фактары аўтэнтыфікацыі" + name: "імя" edit: "рэдагаваць" + security_key: + register: "рэгістрацыя" + delete: 'выдаліць' change_about: title: "Змяніць інфу аба мяне" change_username: @@ -676,19 +680,18 @@ be: google_oauth2: name: "Google" title: "з Google" - message: "Аўтэнтыфікацыя праз Google (праверце, каб блакавальнікі усплываючых вокнаў былі выключаны)" twitter: name: "Twitter" title: "праз Twitter" - message: "Аўтэнтыфікацыя праз Twitter (праверце, каб блакавальнікі усплываючых вокнаў былі выключаны)" instagram: title: "праз Instagram" facebook: title: "праз Facebook" - message: "Аўтэнтыфікацыя праз Facebook (праверце, каб блакавальнікі усплываючых вокнаў былі выключаны)" github: title: "праз GitHub" - message: "Аўтэнтыфікацыя праз GitHub (праверце, каб блакавальнікі усплываючых вокнаў былі выключаны)" + second_factor_toggle: + totp: "Выкарыстоўвайце прыкладанне аутентификационное замест" + backup_code: "Выкарыстоўваць рэзервовы код замест" invites: welcome_to: "Сардэчна запрашаем да сайта %{site_name}!" name_label: "Назва" @@ -1263,6 +1266,7 @@ be: user: "Карыстальнік" title: "API" key: "ключ API" + created: створаны generate: "згенераваць" regenerate: "перегенерировать" revoke: "ануляваць" diff --git a/config/locales/client.bg.yml b/config/locales/client.bg.yml index e0bc6e8ca2..71f4db1355 100644 --- a/config/locales/client.bg.yml +++ b/config/locales/client.bg.yml @@ -612,8 +612,12 @@ bg: second_factor: title: "Двуфакторно удостоверяване" confirm_password_description: "Моля, потвърдете паролата за да продължите" + name: "Име" label: "Код" edit: "Редактирай" + security_key: + register: "Регистриране" + delete: 'Изтрий' change_about: title: "Смяна на За мен" error: "Имаше грешка при промяна на тази стойност." @@ -919,6 +923,8 @@ bg: complete_email_not_found: "Няма акаунт който да съвпада с %{email}" button_ok: "Ок" email_login: + complete_username_found: "Намерихме профил, който съвпада с %{username}, трябва да получите имейл с линк за логин." + complete_email_found: "Намерихме профил, който съвпада с %{email}, трябва да получите имейл с линк за логин." complete_username_not_found: "Няма профил, който да съвпада с потребителскоto име %{username}" complete_email_not_found: "Няма акаунт който да съвпада с %{email}" confirm_title: "Продължете към %{site_name}" @@ -953,20 +959,15 @@ bg: google_oauth2: name: "Google" title: "с Google" - message: "Удостоверяване с Google (проверете за блокирани на поп-ъп прозорци)" twitter: name: "Twitter" title: "с Twitter" - message: "Удостоверяване с Twitter (уверете се, pop up блокерите не са разрешени)" instagram: title: "чрез Instagram" - message: "Удостоверява се връзка с Instagram (уверете се, че поп-блокерите не са включени)" facebook: title: "със Facebook" - message: "Удостоверяване със Facebook (проверете за блокирани на поп-ъп прозорци)" github: title: " с Github" - message: "Удостоверяване с Github (уверете се, pop up блокерите не са разрешени)" invites: welcome_to: "Добре дошли в %{site_name}!" success: "Профилът ви е създаден и вече сте влезли в него." @@ -978,7 +979,6 @@ bg: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" shortcut_modifier_key: shift: "Shift" @@ -2054,6 +2054,7 @@ bg: user: "Потребител" title: "API" key: "API ключ" + created: Създадени generate: "Генраирай" regenerate: "Регенерирай" revoke: "Анулирай" @@ -2343,7 +2344,7 @@ bg: recover_topic: "възстанови темата" delete_post: "изтрий публикацията " impersonate: "представи " - anonymize_user: "анинимизирай потребител" + anonymize_user: "анoнимизирай потребител" roll_up: "върнете IP blocks" change_category_settings: "промени настройките на категорията" delete_category: "изтрий категория" diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml index dee8bc6d73..9702ac9417 100644 --- a/config/locales/client.bs_BA.yml +++ b/config/locales/client.bs_BA.yml @@ -897,7 +897,6 @@ bs_BA: copied_to_clipboard: "Kopirano u međuspremnik" copy_to_clipboard_error: "Pogreška pri kopiranju podataka u međuspremnik" remaining_codes: "Imate preostale rezervne kodove {{count}} ." - use: "Koristite rezervnu šifru" enable_prerequisites: "Morate omogućiti primarni drugi faktor prije generiranja rezervnih kodova." codes: title: "Generirani sigurnosni kodovi" @@ -906,6 +905,7 @@ bs_BA: title: "Two Factor Authentication" enable: "Upravljanje autentifikacijom sa dva faktora" confirm_password_description: "Molimo vas da potvrdite šifru kako bi nastavili" + name: "Ime" label: "Šifra" rate_limit: "Pričekajte prije pokušaja drugog koda za provjeru autentičnosti." enable_description: | @@ -917,7 +917,6 @@ bs_BA: extended_description: | Dvofaktorna autentifikacija dodaje dodatnu sigurnost vašem računu tako što zahtijeva dodatnu jednokratnu oznaku pored vaše lozinke. Tokeni se mogu generirati na Android i iOS uređajima. oauth_enabled_warning: "Imajte na umu da će ulogovanje korištenjem socijalnih mreža biti isključeno u momentu kad uključite two factor authentication (dvofaktorsku ovjeru autentičnosti) na vašem korisničkom računu." - use: "Koristite aplikaciju Autentifikator" enforced_notice: "Morate omogućiti autentifikaciju s dva faktora prije pristupa ovoj web-lokaciji." disable: "onemogući" disable_title: "Onemogući Drugi Faktor" @@ -929,6 +928,8 @@ bs_BA: title: "Autentikatori zasnovani na tokenu" add: "Novi Authenticator" default_name: "Moj Authenticator" + security_key: + delete: 'Izbriši' change_about: title: "Promjeni O meni" error: "Desila se greška prilikom promjene." @@ -1335,10 +1336,8 @@ bs_BA: password: "Šifra" second_factor_title: "Two Factor Authentication" second_factor_description: "Molimo da unesete kod za ovjeru autentičnosti sa vaše aplikacije:" - second_factor_backup: "Prijavite se pomoću rezervnog koda" second_factor_backup_title: "rezevna zaštita za dva faktora" second_factor_backup_description: "Molim ukucajte jedan od vaši rezevni kodova" - second_factor: "Prijavite se pomoću aplikacije Authenticator" email_placeholder: "email ili korisnik" caps_lock_warning: "Uključena su vam velika slova" error: "Nepoznata greška" @@ -1371,23 +1370,18 @@ bs_BA: google_oauth2: name: "Google" title: "koristeći Google" - message: "Prijava preko Google-a (osigurajte da pop up blokeri budu isključeni)" twitter: name: "Twitter" title: "koristeći Twitter" - message: "Prijava preko Twitter-a (osigurajte da pop up blokeri budu isključeni)" instagram: name: "Instagram" title: "koristeći Instagram" - message: "Prijava preko Instagrama (osigurajte da pop up blokeri budu isključeni)" facebook: name: "Facebook" title: "koristeći Facebook" - message: "Prijava preko Facebook-a (osigurajte da pop up blokeri budu isključeni)" github: name: "GitHub" title: "koristeći GitHub" - message: "Prijava preko GitHub-a (osigurajte da pop up blokeri budu isključeni)" invites: accept_title: "Pozivnica" welcome_to: "Dobrodošli na %{site_name}!" @@ -1405,7 +1399,6 @@ bs_BA: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -3062,6 +3055,7 @@ bs_BA: user: "User" title: "API" key: "API Key" + created: Kreiran generate: "Generate" regenerate: "Regenerate" revoke: "Revoke" @@ -3380,7 +3374,6 @@ bs_BA: other: "Tema je {{count}} iza sebe!" compare_commits: "(Pogledajte nove unose)" repo_unreachable: "Nije moguće kontaktirati Git repozitorij ove teme. Poruka o grešci:" - imported_from_archive: "Ova tema je uvezena iz .tar.gz datoteke" scss: text: "Stylesheet" title: "Unesite prilagođeni CSS, prihvaćamo sve važeće CSS i SCSS stilove" diff --git a/config/locales/client.ca.yml b/config/locales/client.ca.yml index e28d249ea7..ba47b17bf6 100644 --- a/config/locales/client.ca.yml +++ b/config/locales/client.ca.yml @@ -880,7 +880,6 @@ ca: copied_to_clipboard: "Copiat al porta-retalls." copy_to_clipboard_error: "Error copiant dades al porta-retalls" remaining_codes: "Us resten {{count}} codis de còpia de seguretat." - use: "Usa un codi de còpia de seguretat" enable_prerequisites: "Cal habilitar un segon factor primari abans de generar codis de còpia de seguretat." codes: title: "Codis de còpia de seguretat generats" @@ -889,6 +888,7 @@ ca: title: "Autenticació de dos factors" enable: "Gestiona l'autenticació de dos factors" confirm_password_description: "Confirmeu la contrasenya per a continuar" + name: "Nom" label: "Codi" rate_limit: "Espereu abans de provar un altre codi d'autenticació." enable_description: | @@ -900,7 +900,6 @@ ca: extended_description: | L'autenticació de dos factors afegeix seguretat addicional al vostre compte exigint un testimoni únic a més de la vostra contrasenya. Es poden generar testimonis en dispositius Android i iOS. oauth_enabled_warning: "Observeu que els inicis de sessió amb xarxes socials seran deshabilitats quan l'autenticació de dos factors s'hagi activat en el vostre compte. " - use: "Fes servir una aplicació d'autenticador" enforced_notice: "Cal que activeu l'autenticació de dos factors abans d'accedir a aquest lloc web." disable: "deshabilita" disable_title: "Deshabilita el segon factor" @@ -912,6 +911,9 @@ ca: title: "Autenticadors basats en testimonis" add: "Autenticador nou" default_name: "El meu autenticador" + security_key: + register: "Registre" + delete: 'Suprimeix' change_about: title: "Canvia Quant a mi" error: "Hi ha hagut un error en canviar aquest valor" @@ -1314,10 +1316,8 @@ ca: password: "Contrasenya" second_factor_title: "Autenticació de dos factors" second_factor_description: "Introduïu el codi d'autenticació de la vostra aplicació:" - second_factor_backup: "Inicia la sessió usant un codi de còpia de seguretat " second_factor_backup_title: "Còpia de seguretat de dos factors" second_factor_backup_description: "Introduïu un dels vostres codis de còpia de seguretat:" - second_factor: "Inicia la sessió usant l'aplicació autenticadora" email_placeholder: "correu electrònic o nom d'usuari" caps_lock_warning: "El bloqueig de majúscula és activat" error: "Error desconegut" @@ -1350,27 +1350,24 @@ ca: google_oauth2: name: "Google" title: "amb Google" - message: "Autenticant amb Google (assegureu-vos que no teniu activats els blocadors de finestres emergents)" twitter: name: "Twitter" title: "amb Twitter" - message: "Autenticant amb Twitter (assegureu-vos que no teniu activats els blocadors de finestres emergents)" instagram: name: "Instagram" title: "amb Instagram" - message: "Autenticant amb Instagram (assegureu-vos que no teniu activats els blocadors de finestres emergents)" facebook: name: "Facebook" title: "amb Facebook" - message: "Autenticant amb Facebook (assegureu-vos que no teniu activats els blocadors de finestres emergents)" github: name: "GitHub" title: "amb GitHub" - message: "Autenticant amb GitHub (assegureu-vos que no teniu activats els blocadors de finestres emergents)" discord: name: "Discord" title: "amb Discord" - message: "Autenticació amb Discord" + second_factor_toggle: + totp: "Utilitzeu una aplicació d'autenticació en comptes d'això" + backup_code: "Utilitzeu un codi de còpia de seguretat en comptes d'això" invites: accept_title: "Invitació" welcome_to: "Benvingut a %{site_name}!" @@ -1388,7 +1385,6 @@ ca: apple_international: "Apple/Internacional" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -3027,6 +3023,7 @@ ca: user: "Usuari" title: "API" key: "Clau API" + created: Creat generate: "Genera" regenerate: "Regenera" revoke: "Revoca" @@ -3349,7 +3346,6 @@ ca: other: "L'aparença és {{count}} commits darrere!" compare_commits: "(Vegeu els commits nous)" repo_unreachable: "No s'ha pogut contactar amb el repositori git d'aquesta aparença. Missatge d'error:" - imported_from_archive: "Aquesta aparença s'ha importat d'un fitxer .tar.gz" scss: text: "CSS" title: "Introduïu el CSS personalitzat. Acceptem tots els estils CSS i SCSS vàlids." @@ -3836,7 +3832,7 @@ ca: cant_delete_all_too_many_posts: one: "No es poden suprimir totes les publicacions perquè l'usuari té més d'%{count} publicació. (delete_all_posts_max)" other: "No es poden suprimir totes les publicacions perquè l'usuari té més de %{count} publicacions. (delete_all_posts_max)" - delete_confirm: "En general, és preferible fer anònims els usuaris en lloc web de suprimir-los, per a evitar que s'esborrin els continguts de les discussions existents.

Esteu SEGUR que voleu suprimir aquest usuari? Això és permanent!" + delete_confirm: "En general, és preferible fer anònims els usuaris en lloc de suprimir-los, per a evitar que s'esborrin els continguts de les discussions existents.

Esteu SEGUR que voleu suprimir aquest usuari? Això és permanent!" delete_and_block: "Suprimeix i bloca aquest correu electrònic i aquesta adreça IP" delete_dont_block: "Només suprimeix" deleting_user: "Suprimint l'usuari..." diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml index d3da86f080..ab9663adbf 100644 --- a/config/locales/client.cs.yml +++ b/config/locales/client.cs.yml @@ -765,6 +765,7 @@ cs: second_factor: title: "Dvoufázové přihlašování" confirm_password_description: "Prosíme před dalším krokem potvrďte své heslo" + name: "Jméno" label: "Kód" rate_limit: "Prosím počkejete před tím, než vyskoušíte další ověřovací kód." disable_description: "Zadejte prosím ověřovací kód z vaší aplikace" @@ -773,6 +774,8 @@ cs: Dvoufaktorová autentizace přidává další bezpečnostní vrstvu k vašemu účtu, protože vedle hesla vyžaduje ještě i zadání jednorázového kódu, vytvořeného na zařízeních Android nebo iOS. oauth_enabled_warning: "Upozorňujeme, že možnost přihlásit se pomocí účtu ze sociální sítě bude vypnuta, jakmila bude na vašem účtu povolena dvoufaktorová autentizace." edit: "Upravit" + security_key: + delete: 'Smazat' change_about: title: "Změna o mně" error: "Došlo k chybě při pokusu změnit tuto hodnotu." @@ -1161,10 +1164,8 @@ cs: password: "Heslo" second_factor_title: "Dvoufázové přihlašování" second_factor_description: "Prosím zadejte autentikační kód z vaší aplikace:" - second_factor_backup: "Přihlásit se záložním kódem" second_factor_backup_title: "Dvoufaktorová autentizace" second_factor_backup_description: "Prosíme zadejte jeden ze svých záložních kódů:" - second_factor: "Přihlásit se aplikací Authenticator" email_placeholder: "emailová adresa nebo uživatelské jméno" caps_lock_warning: "zapnutý Caps Lock" error: "Neznámá chyba" @@ -1195,23 +1196,18 @@ cs: google_oauth2: name: "Google" title: "přes Google" - message: "Přihlašování přes Google (ujistěte se že nemáte zaplé blokování pop up oken)" twitter: name: "Twitter" title: "přes Twitter" - message: "Autentizuji přes Twitter (ujistěte se, že nemáte zablokovaná popup okna)" instagram: name: "Instagram" title: "s Instagramem" - message: "Autentizuji pres Instagram (ujistěte se, že nemáte zablokovaná popup okna)" facebook: name: "Facebook" title: "přes Facebook" - message: "Autentizuji přes Facebook (ujistěte se, že nemáte zablokovaná popup okna)" github: name: "GitHub" title: "přes GitHub" - message: "Autentizuji přes GitHub (ujistěte se, že nemáte zablokovaná popup okna)" discord: name: "Discord" invites: @@ -1231,7 +1227,6 @@ cs: apple_international: "Apple/Mezinárodní" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -2789,6 +2784,7 @@ cs: user: "Uživatel" title: "API" key: "API klíč" + created: Vytvořený generate: "Vygenerovat API klíč" regenerate: "Znovu-vygenerovat API klíč" revoke: "zrušit" diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml index afe04258e6..46c040f77a 100644 --- a/config/locales/client.da.yml +++ b/config/locales/client.da.yml @@ -862,7 +862,6 @@ da: copied_to_clipboard: "Kopierede til udklipsholder" copy_to_clipboard_error: "Fejl ved kopiering til udklipsholder" remaining_codes: "Du har {{count}} backup koder tilbage." - use: "Brug en backup kode" enable_prerequisites: "Du skal aktivere en primær anden faktor, før du genererer backup koder." codes: title: "backup koder genereret" @@ -871,6 +870,7 @@ da: title: "To-faktor godkendelse" enable: "Administrer To-faktor godkendelse" confirm_password_description: "Bekræft dit kodeord for at fortsætte" + name: "Navn" label: "Kode" rate_limit: "Vent venligst, før du prøver en anden godkendelseskode." enable_description: | @@ -882,7 +882,6 @@ da: extended_description: | To-faktor godkendelse tilføjer ekstra sikkerhed til din konto ved at kræve en engangstoken ud over dit kodeord. Tokens kan genereres på Android- og iOS- enheder. oauth_enabled_warning: "Bemærk, at sociale logins deaktiveres, når To-faktor godkendelse er aktiveret på din konto." - use: "Brug Authenticator app" enforced_notice: "Du skal aktivere To-faktor godkendelse før du kan gå ind på denne side." disable: "deaktiver" disable_title: "Deaktiver anden faktor" @@ -894,6 +893,8 @@ da: title: "Token-baserede autentificatorer" add: "Ny autentifikator" default_name: "Min autentifikator" + security_key: + delete: 'Slet' change_about: title: "Skift “Om mig”" error: "Der opstod en fejl i ændringen af denne værdi." @@ -1293,10 +1294,8 @@ da: password: "Adgangskode" second_factor_title: "To-faktor godkendelse" second_factor_description: "Indtast godkendelseskoden fra din app:" - second_factor_backup: "Log ind ved hjælp af en backup kode" second_factor_backup_title: "To-faktor backup" second_factor_backup_description: "Indtast en af dine backup koder:" - second_factor: "Log ind med Authenticator-appen" email_placeholder: "e-mail eller brugernavn" caps_lock_warning: "Caps Lock er sat til" error: "Ukendt fejl" @@ -1329,23 +1328,18 @@ da: google_oauth2: name: "Google" title: "med google" - message: "Validering med Google (vær sikker på at pop up blokeringer er slået fra)" twitter: name: "Twitter" title: "med Twitter" - message: "Logger ind med Twitter (kontrollér at pop-op-blokering ikke er aktiv)" instagram: name: "Instagram" title: "med Instagram" - message: "Validering med Instagram (vær sikker på at pop-up blokering ikke er slået til)" facebook: name: "Facebook" title: "med Facebook" - message: "Logger ind med Facebook (kontrollér at pop-op-blokering ikke er aktiv)" github: name: "GitHub" title: "med GitHub" - message: "Logger ind med GitHub (kontrollér at pop-op-blokering ikke er aktiv)" discord: name: "Discord" title: "med Discord" @@ -1366,7 +1360,6 @@ da: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Første Emoji" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -2999,6 +2992,7 @@ da: user: "Bruger" title: "API" key: "API-nøgle" + created: Oprettet generate: "Generér" regenerate: "Regenerér" revoke: "Tilbagekald" @@ -3318,7 +3312,6 @@ da: other: "Temaet er {{count}} 'commits' bagud!" compare_commits: "(Se nye 'commits')" repo_unreachable: "Kunne ikke kontakte Git-repository for dette tema. Fejl besked:" - imported_from_archive: "Dette tema blev importeret fra en .tar.gz-fil" scss: text: "CSS" title: "Angiv tilpasset CSS - vi accepterer alle gyldige CSS og SCSS-former" diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index c024d073b4..989e556660 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -880,7 +880,7 @@ de: copied_to_clipboard: "Wurde in Zwischenablage kopiert" copy_to_clipboard_error: "Beim Kopieren in die Zwischenablage trat ein Fehler auf" remaining_codes: "Du hast noch {{count}} Wiederherstellungscodes übrig." - use: "Wiederherstellungscode verwenden" + use: "Benutze einen Backup-Code" enable_prerequisites: "Du musst vor dem Generieren von Sicherungscodes einen primären zweiten Faktor aktivieren." codes: title: "Wiederherstellungscodes generiert" @@ -888,7 +888,9 @@ de: second_factor: title: "Zwei-Faktor-Authentifizierung" enable: "Zwei-Faktor-Authentifizierung verwalten" + forgot_password: "Passwort vergessen?" confirm_password_description: "Bitte bestätige dein Passwort um fortzufahren" + name: "Name" label: "Code" rate_limit: "Bitte warte ein wenig, bevor du es mit einem anderen Authentifizierungscode versuchst." enable_description: | @@ -900,7 +902,7 @@ de: extended_description: | Zwei-Faktor-Authentifizierung (2FA) sichert dein Konto zusätzlich ab, indem sie zusätzlich zu deinem Passwort einen einmalig gültigen Code anfordert. Codes können auf Android- und iOS--Geräten generiert werden. oauth_enabled_warning: "Beachte bitte, dass soziale Anmelde-Methoden deaktiviert werden, sobald die Zwei-Faktor-Authentifizierung für dein Konto aktiviert ist." - use: "Authentifizierungs-App verwenden" + use: "Benutze die Authentifizierungs-App" enforced_notice: "Du musst Zwei-Faktor-Authentifizierung aktivieren, bevor du auf diese Seite zugreifen kannst." disable: "deaktivieren" disable_title: "Zweiten Faktor deaktivieren" @@ -908,10 +910,21 @@ de: edit: "Bearbeiten" edit_title: "Zweiten Faktor bearbeiten" edit_description: "Name des zweiten Faktors" + enable_security_key_description: "Wenn Du deinen physischen Sicherheitsschlüssel vorbereitet hast, klicke unten auf die Schaltfläche Registrieren." totp: title: "Token-basierte Authentifikatoren" add: "Neuer Authentifikator" default_name: "Mein Authentifikator" + security_key: + register: "Registrieren" + title: 'Sicherheitsschlüssel' + add: "Registriere den Sicherheitsschlüssel" + default_name: "Hauptsicherheitsschlüssel" + not_allowed_error: "Der Registrierungsvorgang für den Sicherheitsschlüssel ist abgelaufen oder wurde abgebrochen." + already_added_error: "Du hast diesen Sicherheitsschlüssel bereits registriert. Du musst Ihn nicht erneut registrieren.\e" + edit: 'Bearbeite den Sicherheitsschlüssel' + edit_description: 'Name des Sicherheitsschlüssels' + delete: 'Löschen' change_about: title: "„Über mich“ ändern" error: "Beim Ändern dieses Wertes ist ein Fehler aufgetreten." @@ -1317,7 +1330,13 @@ de: second_factor_backup: "Anmeldung mit einem Wiederherstellungscode" second_factor_backup_title: "Zwei-Faktor-Wiederherstellung" second_factor_backup_description: "Bitte gib einen deiner Wiederherstellungs-Codes ein:" - second_factor: "Anmeldung mit Authenticator-App" + second_factor: "Anmeldung mit einer Authentifizierungs-App" + security_key_description: "Wenn Du Deinen physischen Sicherheitsschlüssel vorbereitet hast, klicke unten auf die Schaltfläche \"Mit Sicherheitsschlüssel authentifizieren\"." + security_key_alternative: "Du kannst Deinen Sicherheitsschlüssel nicht finden oder möchtest eine andere Methode verwenden?" + security_key_authenticate: "Mit Sicherheitsschlüssel authentifizieren" + security_key_not_allowed_error: "Der Authentifizierungsprozess für den Sicherheitsschlüssel ist abgelaufen oder wurde abgebrochen." + security_key_no_matching_credential_error: "Im angegebenen Sicherheitsschlüssel wurden keine übereinstimmenden Anmeldeinformationen gefunden." + security_key_support_missing_error: "Dein aktuelles Gerät oder Browser unterstützt die Verwendung von Sicherheitsschlüsseln nicht. Bitte verwende eine andere Methode." email_placeholder: "E-Mail oder Benutzername" caps_lock_warning: "Feststelltaste ist aktiviert" error: "Unbekannter Fehler" @@ -1350,27 +1369,24 @@ de: google_oauth2: name: "Google" title: "mit Google" - message: "Authentifiziere mit Google (stelle sicher, dass keine Pop-up-Blocker aktiviert sind)" twitter: name: "Twitter" title: "mit Twitter" - message: "Authentifiziere mit Twitter (stelle sicher, dass keine Pop-up-Blocker aktiviert sind)" instagram: name: "Instagram" title: "mit Instagram" - message: "Authentifiziere mit Instagram (stelle sicher, dass keine Pop-up-Blocker aktiviert sind)" facebook: name: "Facebook" title: "mit Facebook" - message: "Authentifiziere mit Facebook (stelle sicher, dass keine Pop-up-Blocker aktiviert sind)" github: name: "GitHub" title: "mit GitHub" - message: "Authentifiziere mit GitHub (stelle sicher, dass keine Pop-up-Blocker aktiviert sind)" discord: name: "Discord" title: "mit Discord" - message: "Authenitfizierung mit Discord" + second_factor_toggle: + totp: "Benutze stattdessen eine Authentifizierungs-App" + backup_code: "Benutze stattdessen einen Backup Code" invites: accept_title: "Einladung" welcome_to: "Willkommen bei %{site_name}!" @@ -1388,7 +1404,7 @@ de: apple_international: "Apple" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" + emoji_one: "JoyPixels (früher EmojiOne)" win10: "Windows 10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -3023,6 +3039,7 @@ de: user: "Benutzer" title: "API" key: "API-Key" + created: Erstellt generate: "Erzeugen" regenerate: "Erneuern" revoke: "Widerrufen" @@ -3345,7 +3362,7 @@ de: other: "Theme liegt {{count}} Commits zurück!" compare_commits: "(Siehe neue Beiträge)" repo_unreachable: "Das Git-Repository dieses Themes konnte nicht kontaktiert werden. Fehlermeldung:" - imported_from_archive: "Dieses Theme wurde aus einer .tar.gz-Datei importiert." + imported_from_archive: "Das Design wurde von eine .zip Datei importiert" scss: text: "CSS" title: "Gib benutzerdefiniertes CSS ein, wir akzeptieren alle gültigen CSS und SCSS-Stile" diff --git a/config/locales/client.el.yml b/config/locales/client.el.yml index e6f9df7b4a..b613743aa6 100644 --- a/config/locales/client.el.yml +++ b/config/locales/client.el.yml @@ -627,7 +627,11 @@ el: copied_to_clipboard: "Αντιγράφτηκε στο Clipboard" copy_to_clipboard_error: "Σφάλμα αντιγραφής δεδομένων στο Clipboard" second_factor: + name: "Όνομα" edit: "Επεξεργασία" + security_key: + register: "Εγγραφή" + delete: 'Σβήσιμο' change_about: title: "Άλλαξε τα «σχετικά με εμένα»" error: "Προέκυψε σφάλμα στην αλλαγή της αξίας." @@ -989,20 +993,15 @@ el: google_oauth2: name: "Google" title: "μέσω της Google" - message: "Ταυτοποίηση μέσω της Google (βεβαιώσου πως δεν έχει ενεργοποιηθεί το μπλοκάρισμα των αναδυόμενων παραθύρων)" twitter: name: "Twitter" title: "μέσω του Twitter" - message: "Ταυτοποίηση μέσω του Twitter (βεβαιώσου πως δεν έχει ενεργοποιηθεί το μπλοκάρισμα των αναδυόμενων παραθύρων)" instagram: title: "μέσω του Instagram" - message: "Ταυτοποίηση μέσω του Instagram (βεβαιώσου πως δεν έχει ενεργοποιηθεί το μπλοκάρισμα των αναδυόμενων παραθύρων)" facebook: title: "μέσω του Facebook" - message: "Ταυτοποίηση μέσω του Facebook (βεβαιώσου πως δεν έχει ενεργοποιηθεί το μπλοκάρισμα των αναδυόμενων παραθύρων)" github: title: "μέσω του GitHub" - message: "Ταυτοποίηση μέσω του Github (βεβαιώσου πως δεν έχει ενεργοποιηθεί το μπλοκάρισμα των αναδυόμενων παραθύρων)" invites: accept_title: "Πρόσκληση" welcome_to: "Καλώς ήλθατε στην %{site_name}!" @@ -1020,7 +1019,6 @@ el: apple_international: "Apple/Διεθνής" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -2321,6 +2319,7 @@ el: user: "Χρήστης" title: "API" key: "API Key" + created: Δημιουργήθηκε generate: "Δημιουργία" regenerate: "Αναδημιουγία" revoke: "Ανάκληση" diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 4d73f53c06..f30f1087fb 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -960,7 +960,7 @@ en: copied_to_clipboard: "Copied to Clipboard" copy_to_clipboard_error: "Error copying data to Clipboard" remaining_codes: "You have {{count}} backup codes remaining." - use: "Use a backup code" + use: "Use a backup code" enable_prerequisites: "You must enable a primary second factor before generating backup codes." codes: title: "Backup Codes Generated" @@ -969,7 +969,9 @@ en: second_factor: title: "Two Factor Authentication" enable: "Manage Two Factor Authentication" + forgot_password: "Forgot password?" confirm_password_description: "Please confirm your password to continue" + name: "Name" label: "Code" rate_limit: "Please wait before trying another authentication code." enable_description: | @@ -981,7 +983,7 @@ en: extended_description: | Two factor authentication adds extra security to your account by requiring a one-time token in addition to your password. Tokens can be generated on Android and iOS devices. oauth_enabled_warning: "Please note that social logins will be disabled once two factor authentication has been enabled on your account." - use: "Use Authenticator app" + use: "Use Authenticator app" enforced_notice: "You are required to enable two factor authentication before accessing this site." disable: "disable" disable_title: "Disable Second Factor" @@ -989,10 +991,21 @@ en: edit: "Edit" edit_title: "Edit Second Factor" edit_description: "Second Factor Name" + enable_security_key_description: "When you have your physical security key prepared press the Register button below." totp: title: "Token-Based Authenticators" add: "New Authenticator" default_name: "My Authenticator" + security_key: + register: "Register" + title: 'Security Keys' + add: "Register Security Key" + default_name: "Main Security Key" + not_allowed_error: "The security key registration process either timed out or was cancelled." + already_added_error: "You have already registered this security key. You don’t have to register it again." + edit: 'Edit Security Key' + edit_description: 'Security Key Name' + delete: 'Delete' change_about: title: "Change About Me" @@ -1440,10 +1453,16 @@ en: password: "Password" second_factor_title: "Two Factor Authentication" second_factor_description: "Please enter the authentication code from your app:" - second_factor_backup: "Log in using a backup code" + second_factor_backup: "Log in using a backup code" second_factor_backup_title: "Two Factor Backup" second_factor_backup_description: "Please enter one of your backup codes:" - second_factor: "Log in using Authenticator app" + second_factor: "Log in using Authenticator app" + security_key_description: "When you have your physical security key prepared press the Authenticate with Security Key button below." + security_key_alternative: "Can't find your security key or want to use another method?" + security_key_authenticate: "Authenticate with Security Key" + security_key_not_allowed_error: "The security key authentication process either timed out or was cancelled." + security_key_no_matching_credential_error: "No matching credentials could be found in the provided security key." + security_key_support_missing_error: "Your current device or browser does not support the use of security keys. Please use a different method." email_placeholder: "email or username" caps_lock_warning: "Caps Lock is on" error: "Unknown error" @@ -1478,27 +1497,24 @@ en: google_oauth2: name: "Google" title: "with Google" - message: "Authenticating with Google (make sure pop up blockers are not enabled)" twitter: name: "Twitter" title: "with Twitter" - message: "Authenticating with Twitter (make sure pop up blockers are not enabled)" instagram: name: "Instagram" title: "with Instagram" - message: "Authenticating with Instagram (make sure pop up blockers are not enabled)" facebook: name: "Facebook" title: "with Facebook" - message: "Authenticating with Facebook (make sure pop up blockers are not enabled)" github: name: "GitHub" title: "with GitHub" - message: "Authenticating with GitHub (make sure pop up blockers are not enabled)" discord: name: "Discord" title: "with Discord" - message: "Authenticating with Discord" + second_factor_toggle: + totp: "Use an authenticator app instead" + backup_code: "Use a backup code instead" invites: accept_title: "Invitation" welcome_to: "Welcome to %{site_name}!" @@ -1518,7 +1534,7 @@ en: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" + emoji_one: "JoyPixels (formerly EmojiOne)" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -3286,6 +3302,9 @@ en: user: "User" title: "API" key: "API Key" + created: Created + last_used: Last Used + never_used: (never) generate: "Generate" regenerate: "Regenerate" revoke: "Revoke" @@ -3614,7 +3633,7 @@ en: other: "Theme is {{count}} commits behind!" compare_commits: "(See new commits)" repo_unreachable: "Couldn't contact the Git repository of this theme. Error message:" - imported_from_archive: "This theme was imported from a .tar.gz file" + imported_from_archive: "This theme was imported from a .zip file" scss: text: "CSS" title: "Enter custom CSS, we accept all valid CSS and SCSS styles" diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index b4c0c59ca6..4a4fd6eff2 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -95,6 +95,12 @@ es: x_days: one: "hace %{count} día" other: "hace %{count} días" + x_months: + one: "Hace %{count} meses" + other: "Hace %{count} meses" + x_years: + one: "Hace %{count} años" + other: "Hace %{count} años" later: x_days: one: "%{count} día después" @@ -257,7 +263,7 @@ es: created: "has marcado esta publicación" not_bookmarked: "marcar esta publicación" remove: "Eliminar marcador" - confirm_clear: "¿Estás seguro de que deseas borrar todos tus marcadores en este tema?" + confirm_clear: "¿Estás seguro de que deseas eliminar todos tus marcadores en este tema?" drafts: resume: "Reanudar" remove: "Eliminar" @@ -344,7 +350,7 @@ es: unclaim: help: "eliminar esta reclamación" awaiting_approval: "Esperando aprobación" - delete: "Borrar" + delete: "Eliminar" settings: saved: "Guardado" save_changes: "Guardar cambios" @@ -457,7 +463,7 @@ es: title: "Usuario" approval: title: "La publicación requiere aprobación" - description: "Hemos recibido tu nueva publicación, pero debe ser aprobada por un moderador antes de que aparezca. Por favor sé paciente." + description: "Hemos recibido tu nueva publicación, pero debe ser aprobada por un moderador antes de que aparezca. Por favor, sé paciente." pending_posts: one: "Tienes %{count} publicación pendiente." other: "Tienes {{count}} publicaciones pendientes." @@ -715,7 +721,7 @@ es: button_text: "Descargar todos" confirm: "¿Seguro de que quieres descargar tus publicaciones?" success: "Descarga iniciada, se te notificará por mensaje cuando el proceso se haya completado." - rate_limit_error: "Solo se pueden descargar las publicaciones una vez al día. Por favor inténtalo de nuevo mañana." + rate_limit_error: "Solo se pueden descargar las publicaciones una vez al día. Por favor, inténtalo de nuevo mañana." new_private_message: "Mensaje nuevo" private_message: "Mensaje" private_messages: "Mensajes" @@ -810,7 +816,7 @@ es: delete_account: "Eliminar mi cuenta" delete_account_confirm: "¿Estás seguro de que quieres eliminar permanentemente tu cuenta? ¡Esta acción no puede ser revertida!" deleted_yourself: "Tu cuenta se ha eliminado exitosamente." - delete_yourself_not_allowed: "Por favor contactar a un miembro del staff si deseas que se elimine tu cuenta." + delete_yourself_not_allowed: "Por favor, contacta a un miembro del staff si deseas que se elimine tu cuenta." unread_message_count: "Mensajes" admin_delete: "Eliminar" users: "Usuarios" @@ -874,7 +880,7 @@ es: copied_to_clipboard: "Copiado al portapapeles" copy_to_clipboard_error: "Error al copiar datos al portapapeles" remaining_codes: "Tienes {{count}} códigos de respaldo restantes." - use: "Usar un código de respaldo" + use: "Usar un código de respaldo" enable_prerequisites: "Debes habilitar un segundo factor primario antes de generar códigos de respaldo." codes: title: "Códigos de respaldo generados" @@ -882,19 +888,21 @@ es: second_factor: title: "Autenticación en dos pasos" enable: "Gestionar autenticación en dos pasos" - confirm_password_description: "Por favor confirma tu contraseña para continuar" + forgot_password: "¿Olvidaste tu contraseña?" + confirm_password_description: "Por favor, confirma tu contraseña para continuar" + name: "Nombre" label: "Código" - rate_limit: "Por favor espera antes de intentar utilizar otro código de autenticación." + rate_limit: "Por favor, espera antes de intentar utilizar otro código de autenticación." enable_description: | Escanea este código QR en una aplicación que lo soporte (AndroidiOS) e ingresa el código de autenticación. - disable_description: "Por favor ingresa el código de autenticación que aparece en tu aplicación" + disable_description: "Por favor, ingresa el código de autenticación que aparece en tu aplicación" show_key_description: "Ingresa el código manualmente" short_description: | Protege tu cuenta mediante códigos de respaldo de un solo uso. extended_description: | La verificación en dos pasos incrementa la seguridad de tu cuenta al requerir un código de único solo uso además de tu contraseña. Los códigos se pueden generar tanto en dispositivos Android como iOS. - oauth_enabled_warning: "Por favor ten en cuenta que el acceso a tu cuenta a través de redes sociales se inhabilitará si activas la autenticación en dos pasos." - use: "Usar la app Authenticator" + oauth_enabled_warning: "Por favor, ten en cuenta que el acceso a tu cuenta a través de redes sociales se inhabilitará si activas la autenticación en dos pasos." + use: "Usar app Authenticator" enforced_notice: "Se necesita que actives la autenticación en dos pasos antes de acceder a este sitio." disable: "inhabilitar" disable_title: "Inhabilitar segundo factor" @@ -902,10 +910,21 @@ es: edit: "Editar" edit_title: "Editar segundo factor" edit_description: "Nombre del segundo factor" + enable_security_key_description: "Cuando tengas tu clave de seguridad física preparada, presione el botón de registro que se encuentra debajo." totp: title: "Autenticación basada en tokens" add: "Nuevo autenticador" default_name: "Mi autenticador" + security_key: + register: "Registrar" + title: 'Claves de seguridad' + add: "Clave de seguridad de registro" + default_name: "Clave de seguridad principal" + not_allowed_error: "El proceso de registro de clave de seguridad fue cancelado o se agotó el tiempo." + already_added_error: "Ya registraste esta clave de seguridad. No tienes que registrarla de nuevo." + edit: 'Editar clave de seguridad' + edit_description: 'Nombre de la clave de seguridad' + delete: 'Eliminar' change_about: title: "Cambiar «Acerca de mí»" error: "Ha ocurrido un error al cambiar este valor." @@ -918,8 +937,8 @@ es: title: "Cambiar correo electrónico" taken: "Lo sentimos, ese correo electrónico no está disponible." error: "Ha ocurrido un error al cambiar tu correo electrónico. ¿Tal vez esa dirección ya se encuentra en uso?" - success: "Te hemos enviado un correo electrónico a esa dirección. Por favor sigue las instrucciones de confirmación." - success_staff: "Hemos enviado un correo electrónico a tu dirección actual. Por favor sigue las instrucciones de confirmación." + success: "Te hemos enviado un correo electrónico a esa dirección. Por favor, sigue las instrucciones de confirmación." + success_staff: "Hemos enviado un correo electrónico a tu dirección actual. Por favor, sigue las instrucciones de confirmación." change_avatar: title: "Cambiar tu imagen de perfil" gravatar: "Gravatar, basado en" @@ -945,7 +964,7 @@ es: sso_override_instructions: "El correo electrónico puede actualizarse desde el proveedor de SSO." instructions: "Nunca se mostrará al público." ok: "Te enviaremos un correo electrónico para confirmar" - invalid: "Por favor ingresa una dirección de correo electrónico válida" + invalid: "Por favor, ingresa una dirección de correo electrónico válida" authenticated: "Tu dirección de correo electrónico ha sido autenticada por {{provider}}" frequency_immediately: "Te enviaremos un correo electrónico inmediatamente si no has leído el asunto por el cual te estamos enviando el correo." frequency: @@ -1158,9 +1177,9 @@ es: more_badges: "Más insignias" top_links: "Enlaces destacados" no_links: "No hay enlaces aún." - most_liked_by: "Los que dieron más me gusta" - most_liked_users: "Con más me gusta" - most_replied_to_users: "A quienes más respondiste" + most_liked_by: "Recibió mas me gusta de" + most_liked_users: "Dio más me gusta a" + most_replied_to_users: "Respondió más a" no_likes: "No hay ningún me gusta aún." top_categories: "Categorías destacadas" topics: "Temas" @@ -1192,7 +1211,7 @@ es: unknown: "Error" not_found: "Página no encontrada" desc: - network: "Por favor revisa tu conexión." + network: "Por favor, revisa tu conexión." network_fixed: "Parece que ha vuelto." server: "Código de error: {{status}}" forbidden: "No tienes permitido ver esto." @@ -1280,14 +1299,14 @@ es: title: "Restablecer contraseña" action: "Olvidé mi contraseña" invite: "Ingresa tu nombre de usuario o tu dirección de correo electrónico, y te enviaremos un correo para reestablecer tu contraseña." - reset: "Restablecer Contraseña" + reset: "Restablecer contraseña" complete_username: "Si una cuenta coincide con el nombre de usuario %{username}, en breve deberías recibir un correo electrónico con las instrucciones para reestablecer tu contraseña." complete_email: "Si una cuenta coincide con %{email}, en breve deberías recibir un correo electrónico con las instrucciones para reestablecer tu contraseña." complete_username_found: "Encontramos una cuenta que coincide con el usuario %{username}, en breve deberías recibir un correo electrónico con instrucciones para restablecer tu contraseña." complete_email_found: "Encontramos una cuenta que coincide con %{email}, deberías recibir en breve un correo electrónico con instrucciones para restablecer tu contraseña." complete_username_not_found: "No hay ninguna cuenta que coincida con el nombre de usuario %{username}" complete_email_not_found: "No hay ninguna cuenta que coincida con el correo electrónico %{email}" - help: "¿No te ha llegado el correo? Asegúrate de comprobar primero tu carpeta de correo no deseado.

¿No estás seguro de qué correo has usado? Ingresa tu correo electrónico y te avisaremos si lo tenemos registrado.

Si no tienes acceso al correo electrónico asociado a tu cuenta, por favor contacta a nuestro amable staff.

" + help: "¿No te ha llegado el correo? Asegúrate de comprobar primero tu carpeta de correo no deseado.

¿No estás seguro de qué correo has usado? Ingresa tu correo electrónico y te avisaremos si lo tenemos registrado.

Si no tienes acceso al correo electrónico asociado a tu cuenta, por favor, contacta a nuestro amable staff.

" button_ok: "OK" button_help: "Ayuda" email_login: @@ -1307,18 +1326,24 @@ es: username: "Usuario" password: "Contraseña" second_factor_title: "Autenticación en dos pasos" - second_factor_description: "Por favor ingresa el código de autenticación desde tu aplicación:" - second_factor_backup: "Iniciar sesión usando un código de respaldo" + second_factor_description: "Por favor, ingresa el código de autenticación desde tu aplicación:" + second_factor_backup: "Iniciar sesión utilizando un código de respaldo" second_factor_backup_title: "Respaldo de la autenticación en dos pasos" second_factor_backup_description: "Por favor, ingresa uno de los códigos de respaldo:" - second_factor: "Inicia sesión usando la app Authenticator" + second_factor: "Iniciar sesión utilizando la app Authenticator" + security_key_description: "Cuando tengas tu clave de seguridad física preparada, presiona el botón de autenticar con clave de seguridad que se encuentra debajo." + security_key_alternative: "¿No encuentras tu clave de seguridad o quieres utilizar otro método?" + security_key_authenticate: "Autenticar con clave de seguridad" + security_key_not_allowed_error: "La autenticación de la clave de seguridad fue cancelada o se agotó el tiempo." + security_key_no_matching_credential_error: "No se encontraron credenciales que coincidan en la clave de seguridad provista." + security_key_support_missing_error: "Tu dispositivo o navegador actual no soporta el uso de claves de seguridad. Por favor, utiliza un método diferente." email_placeholder: "dirección de correo electrónico o nombre de usuario" caps_lock_warning: "El bloqueo de mayúsculas está activado" error: "Error desconocido" cookies_error: "Parece que tu navegador tiene deshabilitados los cookies. Es posible que no puedas iniciar sesión sin habilitarlos primero." - rate_limit: "Por favor espera un poco antes intentar iniciar sesión de nuevo." - blank_username: "Por favor ingresa tu correo electrónico o nombre de usuario." - blank_username_or_password: "Por favor ingresa tu correo electrónico o nombre de usuario y tu contraseña." + rate_limit: "Por favor, espera un poco antes intentar iniciar sesión de nuevo." + blank_username: "Por favor, ingresa tu correo electrónico o nombre de usuario." + blank_username_or_password: "Por favor, ingresa tu correo electrónico o nombre de usuario y tu contraseña." reset_password: "Restablecer contraseña" logging_in: "Iniciando Sesión..." or: "O" @@ -1326,45 +1351,42 @@ es: awaiting_activation: "Tu cuenta está pendiente de activación, usa el enlace de «olvidé contraseña» para recibir otro correo electrónico de activación." awaiting_approval: "Tu cuenta todavía no ha sido aprobada por un miembro del staff. Recibirás un correo electrónico cuando sea aprobada." requires_invite: "Lo sentimos, solo se puede acceder a este foro mediante invitación." - not_activated: "No puedes iniciar sesión todavía. Anteriormente te hemos enviado un correo electrónico de activación a la dirección {{sentTo}}. Por favor sigue las instrucciones que allí se encuentran para activar tu cuenta." + not_activated: "No puedes iniciar sesión todavía. Anteriormente te hemos enviado un correo electrónico de activación a la dirección {{sentTo}}. Por favor, sigue las instrucciones que allí se encuentran para activar tu cuenta." not_allowed_from_ip_address: "No puedes iniciar sesión desde esa dirección IP." admin_not_allowed_from_ip_address: "No puedes iniciar sesión como administrador desde esta dirección IP." resend_activation_email: "Has clic aquí para enviar el correo electrónico de activación nuevamente." - omniauth_disallow_totp: "Tu cuenta tiene activada la autenticación en dos pasos. Por favor ingresa usando tu contraseña." + omniauth_disallow_totp: "Tu cuenta tiene activada la autenticación en dos pasos. Por favor, ingresa usando tu contraseña." resend_title: "Volver a enviar el correo electrónico de activación" change_email: "Cambiar dirección de correo electrónico" provide_new_email: "Ingresa una dirección de correo electrónico nueva y te reenviaremos el correo de confirmación." submit_new_email: "Actualizar dirección de correo electrónico" sent_activation_email_again: "Te hemos enviado otro correo electrónico de activación a {{currentEmail}}. Podría tardar algunos minutos en llegar; asegúrate de revisar la carpeta de correo no deseado." sent_activation_email_again_generic: "Te hemos enviado otro correo electrónico de activación. Podría tardar algunos minutos en llegar. Asegúrate de revisar la carpeta de correo no deseado." - to_continue: "Por favor inicia sesión" + to_continue: "Por favor, inicia sesión" preferences: "Debes iniciar sesión para poder cambiar tus preferencias de usuario." forgot: "No me acuerdo de los detalles de mi cuenta." not_approved: "Tu cuenta aún no ha sido aprobada. Se te notificará por correo electrónico cuando todo esté listo para que inicies sesión." google_oauth2: name: "Google" title: "con Google" - message: "Autenticando con Google (asegúrate de no tener habilitado ningún bloqueador de elementos emergentes)" twitter: name: "Twitter" title: "con Twitter" - message: "Autenticando con Twitter (asegúrate de no tener habilitado ningún bloqueador de elementos emergentes)" instagram: name: "Instagram" title: "con Instagram" - message: "Autenticando con Instagram (asegúrate de no tener habilitado ningún bloqueador de elementos emergentes)" facebook: name: "Facebook" title: "con Facebook" - message: "Autenticando con Facebook (asegúrate de no tener habilitado ningún bloqueador de elementos emergentes)" github: name: "GitHub" title: "con GitHub" - message: "Autenticando con GitHub (asegúrate de no tener habilitado ningún bloqueador de elementos emergentes)" discord: name: "Discord" title: "con Discord" - message: "Autenticando con Discord" + second_factor_toggle: + totp: "Usar una aplicación de autenticación en su lugar" + backup_code: "Usar un código de respaldo en su lugar" invites: accept_title: "Invitación" welcome_to: "¡Bienvenido a %{site_name}!" @@ -1382,7 +1404,7 @@ es: apple_international: "Apple/Internacional" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" + emoji_one: "JoyPixels (anteriormente EmojiOne)" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -1471,14 +1493,14 @@ es: reference_topic_title: "RE: {{title}}" error: title_missing: "Es necesario un título" - title_too_short: "El título debe tener por lo menos {{min}} caracteres." + title_too_short: "El título debe tener por lo menos {{min}} caracteres" title_too_long: "El título no puede tener más de {{max}} caracteres." - post_missing: "Las publicaciones no pueden estar vacías" + post_missing: "La publicación no puede estar vacía" post_length: "La publicación debe tener por lo menos {{min}} caracteres." try_like: "¿Has probado el botón {{heart}}?" category_missing: "Debes escoger una categoría." tags_missing: "Debes seleccionar al menos {{count}} etiquetas" - topic_template_not_modified: "Por favor agrega detalles y especificaciones a tu tema editando la plantilla de tema." + topic_template_not_modified: "Por favor, agrega detalles y especificaciones a tu tema editando la plantilla de tema." save_edit: "Guardar edición" overwrite_edit: "Sobrescribir edición" reply_original: "Responder en el tema original" @@ -1678,7 +1700,7 @@ es: searching: "Buscando ..." post_format: "#{{post_number}} de {{username}}" results_page: "Resultados de búsqueda de «{{term}}»" - more_results: "Hay más resultados. Por favor restringe los criterios de búsqueda." + more_results: "Hay más resultados. Por favor, restringe los criterios de búsqueda." cant_find: "¿No puedes encontrar lo que estás buscando?" start_new_topic: "¿Y si creas un nuevo tema?" or_search_google: "O prueba buscar a través de Google:" @@ -1712,12 +1734,12 @@ es: private: en mis mensajes bookmarks: he guardado first: son la primera publicación - pinned: destacados - unpinned: no destacados + pinned: son destacados + unpinned: son no destacados seen: he leído unseen: no he leído wiki: son tipo wiki - images: incluye imagen(es) + images: incluyen imágenes all_tags: todas las etiquetas anteriores statuses: label: Donde los temas @@ -1826,7 +1848,7 @@ es: login_required: "Debes iniciar sesión para poder ver este tema." server_error: title: "No se pudo cargar el tema" - description: "Lo sentimos, no pudimos cargar el tema. Posiblemente se debe a problemas de conexión. Por favor inténtalo nuevamente más tarde. Si el problema persiste, por favor contáctanos." + description: "Lo sentimos, no pudimos cargar el tema. Posiblemente se debe a problemas de conexión. Por favor, inténtalo nuevamente más tarde. Si el problema persiste, por favor contáctanos." not_found: title: "Tema no encontrado" description: "Lo sentimos, no pudimos encontrar ese tema. ¿Tal vez fue eliminado por un moderador?" @@ -1858,7 +1880,7 @@ es: suggest_create_topic: "¿Por qué no creas un tema?" jump_reply_up: saltar a la primera respuesta jump_reply_down: saltar a la última respuesta - deleted: "El tema ha sido borrado" + deleted: "El tema ha sido eliminado" topic_status_update: title: "Temporizador de temas" save: "Configurar temporizador" @@ -1868,7 +1890,7 @@ es: when: "Cuando:" public_timer_types: Temporizadores de temas private_timer_types: Temporizadores de tema del usuario - time_frame_required: Por favor selecciona un plazo + time_frame_required: "Por favor, selecciona un plazo" auto_update_input: none: "Selecciona el plazo" later_today: "Más tarde durante el día de hoy" @@ -1897,7 +1919,7 @@ es: auto_close: title: "Cerrar tema automaticamente" label: "Horas de cierre automático del tema:" - error: "Por favor ingrese un valor válido." + error: "Por favor, ingresa un valor válido." based_on_last_post: "No cerrar hasta que la última publicación en el tema tenga por lo menos esta antigüedad." auto_delete: title: "Eliminar tema automaticamente" @@ -1910,7 +1932,7 @@ es: auto_close: "Este tema se cerrará automáticamente %{timeLeft}." auto_publish_to_category: "Este tema se publicará en #%{categoryName} %{timeLeft}." auto_close_based_on_last_post: "Este tema se cerrará %{duration} después de la última respuesta." - auto_delete: "Este tema se borrará automáticamente %{timeLeft}." + auto_delete: "Este tema se eliminará automáticamente %{timeLeft}." auto_bump: "La fecha de este tema se actualizará %{timeLeft}." auto_reminder: "Te recordaremos sobre este tema %{timeLeft}." auto_close_title: "Configuración de cierre automático" @@ -2020,7 +2042,7 @@ es: success_message: "Has reportado este tema correctamente." make_public: title: "Convertir en tema público" - choose_category: "Por favor elige una categoría para el tema público:" + choose_category: "Por favor, elige una categoría para el tema público:" feature_topic: title: "Características de este tema" pin: "Hacer que este tema aparezca de primero en la categoría {{categoryLink}} hasta" @@ -2101,7 +2123,7 @@ es: radio_label: "Tema existente" instructions: one: "Por favor escoge el tema al que quieres mover ese post." - other: "Por favor escoge el tema al que quieres mover estas {{count}} publicaciones." + other: "Por favor, escoge el tema al que quieres mover estas {{count}} publicaciones." move_to_new_message: title: "Mover a un mensaje nuevo" action: "mover a un mensaje nuevo" @@ -2118,7 +2140,7 @@ es: participants: "Participantes" instructions: one: "Por favor, selecciona el mensaje al que te gustaría mover el mensaje." - other: "Por favor selecciona el mensaje al que te gustaría mover los {{count}} mensajes." + other: "Por favor, selecciona el mensaje al que te gustaría mover los {{count}} mensajes." merge_posts: title: "Fusionar las publicaciones seleccionadas" action: "fusionar las publicaciones seleccionadas" @@ -2130,13 +2152,13 @@ es: placeholder: "nombre de usuario del nuevo dueño" instructions: one: "Por favor escoge el nuevo dueño del post de @{{old_user}}" - other: "Por favor escoge el nuevo dueño de las {{count}} publicaciones de @{{old_user}}" + other: "Por favor, escoge el nuevo dueño de las {{count}} publicaciones de @{{old_user}}" change_timestamp: title: "Cambiar marca horaria..." action: "cambiar marca horaria" invalid_timestamp: "La marca horaria no puede ser en el futuro" error: "Hubo un error al cambiar la marca horaria de este tema." - instructions: "Por favor selecciona la nueva marca horaria del tema. Las publicaciones en el tema se actualizarán para mantener la diferencia de tiempo." + instructions: "Por favor, selecciona la nueva marca horaria del tema. Las publicaciones en el tema se actualizarán para mantener la diferencia de tiempo." multi_select: select: "seleccionar" selected: "seleccionados ({{count}})" @@ -2199,9 +2221,9 @@ es: one: "A tí y a una persona le ha gustado este mensaje" other: "A tí y a otros {{count}} les han gustado este mensaje" errors: - create: "Lo sentimos, se produjo un error al crear tu publicación. Por favor inténtalo de nuevo." - edit: "Lo sentimos, se produjo un error al editar tu publicación. Por favor inténtalo de nuevo." - upload: "Lo sentimos, se produjo un error al subir este archivo. Por favor inténtalo de nuevo." + create: "Lo sentimos, se produjo un error al crear tu publicación. Por favor, inténtalo de nuevo." + edit: "Lo sentimos, se produjo un error al editar tu publicación. Por favor, inténtalo de nuevo." + upload: "Lo sentimos, se produjo un error al subir este archivo. Por favor, inténtalo de nuevo." file_too_large: "Lo sentimos, ese archivo es demasiado grande (el tamaño máximo es {{max_size_kb}} kb). ¿Por qué no lo subes a un servicio de almacenamiento en la nube y compartes el enlace luego?" too_many_uploads: "Lo sentimos, solo puedes subir un archivo a la vez." too_many_dragged_and_dropped_files: "Lo sentimos, solo puedes subir {{max}} archivos a la vez." @@ -2258,9 +2280,9 @@ es: lock_post_description: "impedir que el usuario que realizó esta publicación la edite" unlock_post: "Desbloquear publicación" unlock_post_description: "permitir que el usuario que realizó esta publicación la edite" - delete_topic_disallowed_modal: "No tienes permiso para borrar este tema. Si de verdad quieres que se elimine, repórtalo y explica tus motivos a los moderadores." - delete_topic_disallowed: "no tienes permiso para borrar este tema" - delete_topic: "borrar tema" + delete_topic_disallowed_modal: "No tienes permiso para eliminar este tema. Si de verdad quieres que se elimine, repórtalo y explica tus motivos a los moderadores." + delete_topic_disallowed: "no tienes permiso para eliminar este tema" + delete_topic: "eliminar tema" add_post_notice: "Añadir aviso del staff" remove_post_notice: "Eliminar aviso del staff" remove_timer: "quitar temporizador" @@ -2375,15 +2397,15 @@ es: name_placeholder: "Una o dos palabras máximo" color_placeholder: "Cualquier color web" delete_confirm: "¿Estás seguro de que quieres eliminar esta categoría?" - delete_error: "Se produjo un error al borrar la categoría." + delete_error: "Se produjo un error al eliminar la categoría." list: "Lista de categorías" - no_description: "Por favor agrega una descripción para esta categoría." + no_description: "Por favor, agrega una descripción para esta categoría." change_in_category_topic: "Editar descripción" already_used: "Este color ya ha sido usado para otra categoría" security: "Seguridad" special_warning: "Aviso: esta categoría se ajusta por defecto y las opciones de seguridad no pueden ser editadas. Si no deseas utilizarla, elimínala en vez de reutilizarla." uncategorized_security_warning: "Esta categoría es especial: se usa para temas que no tienen una categoría asignada y y no puede tener ajustes de seguridad." - uncategorized_general_warning: 'Esta categoría es especial. Se utiliza como la categoría predeterminada para los temas nuevos que no tienen una categoría seleccionada. Si deseas evitar este comportamiento y forzar la selección de categorías, por favor desactiva la opción aquí. Si deseas cambiar el nombre o la descripción, ve a Personalizar / Contenido de texto.' + uncategorized_general_warning: 'Esta categoría es especial. Se utiliza como la categoría predeterminada para los temas nuevos que no tienen una categoría seleccionada. Si deseas evitar este comportamiento y forzar la selección de categorías, por favor, desactiva la opción aquí. Si deseas cambiar el nombre o la descripción, ve a Personalizar / Contenido de texto.' pending_permission_change_alert: "No has agregado a %{group} a esta categoría. Haz clic en este botón para agregarlos." images: "Imágenes" email_in: "Dirección de correo electrónico personalizada para el correo entrante:" @@ -2470,7 +2492,7 @@ es: official_warning: "Advertencia oficial" delete_spammer: "Eliminar spammer" delete_confirm_MF: "Estás a punto de eliminar {POSTS, plural, one {1 post} other {# posts}} y {TOPICS, plural, one {1 topic} other {# topics}} de este usuario, eliminar su cuenta, bloquear registros desde su dirección IP {ip_address}, y añadir su dirección de correo electrónico {email} a la lista de bloqueo permanente. ¿Estás seguro de que este usuario es un spammer?" - yes_delete_spammer: "Sí, borrar spammer" + yes_delete_spammer: "Sí, eliminar spammer" ip_address_missing: "(N/D)" hidden_email_address: "(oculto)" submit_tooltip: "Enviar el reporte privado" @@ -2616,8 +2638,8 @@ es: other: "{{categoryName}} ({{count}})" help: "temas recientes en la categoría {{categoryName}}" top: - title: "Parte superior" - help: "los temas más con más actividad en el último año, mes, semana o día" + title: "Destacado" + help: "los temas con más actividad en el último año, mes, semana o día" all: title: "Siempre" yearly: @@ -2636,8 +2658,8 @@ es: this_month: "Mes" this_week: "Semana" today: "Hoy" - other_periods: "ver parte superior" - browser_update: 'Desafortunadamente tu navegador es demasiado antiguo para funcionar en este sitio. Por favor actualiza tu navegador.' + other_periods: "ver los destacados" + browser_update: 'Desafortunadamente tu navegador es demasiado antiguo para funcionar en este sitio. Por favor, actualiza tu navegador.' permission_types: full: "Crear / Responder / Ver" create_post: "Responder / Ver" @@ -2664,7 +2686,7 @@ es: new: "%{shortcut} Nuevos" unread: "%{shortcut} Sin leer" categories: "%{shortcut} Categorías" - top: "%{shortcut} Parte superior" + top: "%{shortcut} Destacado" bookmarks: "%{shortcut} Marcadores" profile: "%{shortcut} Perfil" messages: "%{shortcut} Mensajes" @@ -2707,7 +2729,7 @@ es: flag: "%{shortcut} Reportar publicación" bookmark: "%{shortcut} Guardar publicación en marcadores" edit: "%{shortcut} Editar publicación" - delete: "%{shortcut} Borrar publicación" + delete: "%{shortcut} Eliminar publicación" mark_muted: "%{shortcut} Silenciar tema" mark_regular: "%{shortcut} Seguimiento normal del tema normal (por defecto)" mark_tracking: "%{shortcut} Seguir tema" @@ -2720,7 +2742,7 @@ es: other: "Insignia ganada %{count} veces" granted_on: "Concedido el %{date}" others_count: "Otras personas con esta insignia (%{count})" - title: Insignia + title: Insignias allow_title: "Puedes usar esta insignia como título" multiple_grant: "Puedes ganar esta insignia varias veces" badge_count: @@ -2766,8 +2788,8 @@ es: delete_tag: "Eliminar etiqueta" delete_confirm: one: "¿Estás seguro de querer borrar esta etiqueta y eliminarla de %{count} tema asignado?" - other: "¿Estás seguro de que quieres borrar esta etiqueta y eliminarla de los {{count}} temas a los que está asignada?" - delete_confirm_no_topics: "¿Estás seguro de que quieres borrar esta etiqueta?" + other: "¿Estás seguro de que quieres eliminar esta etiqueta y quitarla de los {{count}} temas a los que está asignada?" + delete_confirm_no_topics: "¿Estás seguro de que quieres eliminar esta etiqueta?" rename_tag: "Renombrar etiqueta" rename_instructions: "Elige un nuevo nombre para la etiqueta:" sort_by: "Ordenar por:" @@ -2871,7 +2893,7 @@ es: up_to_date: "¡Estás actualizado!" critical_available: "Actualización crítica disponible." updates_available: "Hay actualizaciones disponibles." - please_upgrade: "¡Por favor actualiza!" + please_upgrade: "¡Por favor, actualiza!" no_check_performed: "Todavía no se ha realizado ninguna revisión de actualizaciones. Asegúrate de que sidekiq esté funcionando." stale_data: "No se ha realizado recientemente ninguna revisión de actualizaciones. Asegúrate de que sidekiq esté funcionando." version_check_pending: "Parece que has actualizado recientemente. ¡Fantástico!" @@ -2901,7 +2923,7 @@ es: page_views: "Vistas de página" page_views_short: "Vistas de página" show_traffic_report: "Mostrar informe detallado del tráfico" - community_health: Salud de la Comunidad + community_health: Salud de la comunidad moderators_activity: Actividad de moderación whats_new_in_discourse: "¿Qué hay de nuevo en Discourse?" activity_metrics: Medidas de actividad @@ -2912,9 +2934,9 @@ es: reports_tab: "Informes" report_filter_any: "cualquiera" disabled: Desactivado - timeout_error: "Lo sentimos, la solicitud está durando demasiado, por favor selecciona un periodo más corto" + timeout_error: "Lo sentimos, la solicitud está durando demasiado. Por favor, selecciona un periodo más corto" exception_error: "Lo sentimos, se produjo un error al ejecutar la consulta" - too_many_requests: Has realizado esta acción demasiadas veces. Por favor espera antes de intentarlo de nuevo. + too_many_requests: "Has realizado esta acción demasiadas veces. Por favor, espera antes de intentarlo de nuevo." not_found_error: "Lo sentimos, este reporte no existe" filter_reports: Filtrar informes reports: @@ -2949,7 +2971,7 @@ es: category: label: Categoría commits: - latest_changes: "Cambios recientes: ¡por favor actualiza a menudo!" + latest_changes: "Cambios recientes: ¡por favor, actualiza a menudo!" by: "por" groups: new: @@ -3002,9 +3024,9 @@ es: refresh: "Actualizar" about: "Edita la membresía de tu grupo y sus miembros aquí" group_members: "Miembros del grupo" - delete: "Borrar" - delete_confirm: "¿Borrar este grupo?" - delete_failed: "No se pudo borrar el grupo. Si este es un grupo automático, no se puede destruir." + delete: "Eliminar" + delete_confirm: "¿Eliminar este grupo?" + delete_failed: "No se pudo eliminar el grupo. Si este es un grupo automático, no se puede destruir." delete_owner_confirm: "¿Quitar los privilegios de propietario a «%{username}»?" add: "Añadir" custom: "Personalizado" @@ -3021,6 +3043,7 @@ es: user: "Usuario" title: "API" key: "Clave de API" + created: Creado generate: "Generar clave de API" regenerate: "Regenerar" revoke: "Revocar" @@ -3164,7 +3187,7 @@ es: error: "Se produjo un error al subir el archivo «{{filename}}»: {{message}}" operations: is_running: "Hay una operación en proceso actualmente..." - failed: "La {{operation}} falló. Por favor revisa los registros" + failed: "La {{operation}} falló. Por favor, revisa los registros" cancel: label: "Cancelar" title: "Cancelar la operación actual" @@ -3179,8 +3202,8 @@ es: title: "Enviar correo electrónico con enlace de descarga" alert: "El enlace para descargar esta copia de respaldo se te envió por correo electrónico." destroy: - title: "Borrar la copia de respaldo" - confirm: "¿Estás seguro de que quieres borrar esta copia de respaldo?" + title: "Quitar la copia de respaldo" + confirm: "¿Estás seguro de que quieres destruir esta copia de respaldo?" restore: is_disabled: "Restaurar está deshabilitado en la configuración del sitio." label: "Restaurar" @@ -3196,7 +3219,7 @@ es: backup_storage_error: "Error de acceso al almacenamiento de respaldos: %{error_message}" export_csv: success: "Exportación iniciada, se te notificará a través de un mensaje cuando el proceso se haya completado." - failed: "La exportación falló. Por favor revisa los registros." + failed: "La exportación falló. Por favor, revisa los registros." button_text: "Exportar" button_title: user: "Exportar la lista completa de usuarios en formato CSV." @@ -3252,8 +3275,8 @@ es: create_name: "Nombre" long_title: "Modificar los colores, CSS y contenidos HTML de su sitio" edit: "Editar" - edit_confirm: "Este es un tema remoto, si editas CSS/HTML, los cambios se borrarán la próxima vez que actualices el tema." - update_confirm: "Estos cambios locales se borrarán por la actualización. ¿Estás seguro de que quieres continuar?" + edit_confirm: "Este es un tema remoto, si editas CSS/HTML, los cambios se eliminarán la próxima vez que actualices el tema." + update_confirm: "Estos cambios locales se eliminarán por la actualización. ¿Estás seguro de que quieres continuar?" update_confirm_yes: "Sí, continuar con la actualización" common: "Común" desktop: "Escritorio" @@ -3302,7 +3325,7 @@ es: css_html: "Personalizar CSS/HTML" edit_css_html: "Editar CSS/HTML" edit_css_html_help: "No has editado ningún CSS o HTML" - delete_upload_confirm: "¿Borrar este archivo? (¡El tema CSS puede dejar de funcionar!)" + delete_upload_confirm: "¿Eliminar este archivo? (¡El tema CSS puede dejar de funcionar!)" import_web_tip: "Repositorio que contiene el tema" import_web_advanced: "Avanzado..." import_file_tip: "archivo .tar.gz, .zip o .dcstyle.json que contiene un tema" @@ -3343,7 +3366,7 @@ es: other: "¡El tema está {{count}} commits detrás!" compare_commits: "(Ver nuevos commits)" repo_unreachable: "No se ha podido contactar el repositorio Git de este tema. Mensaje de error:" - imported_from_archive: "Este tema se importó de un archivo .tar.gz" + imported_from_archive: "Este tema se importó desde un archivo .zip" scss: text: "CSS" title: "Ingresa el CSS personalizado, aceptamos todos los estilos válidos de CSS y SCSS" @@ -3433,7 +3456,7 @@ es: preview_digest: "Vista previa de resumen" advanced_test: title: "Prueba avanzada" - desc: "Observa cómo Discourse procesa los correos electrónicos recibidos. Para poder procesar correctamente el correo electrónico, por favor pega acá abajo el mensaje de correo electrónico original completo." + desc: "Observa cómo Discourse procesa los correos electrónicos recibidos. Para poder procesar correctamente el correo electrónico, por favor, pega acá abajo el mensaje de correo electrónico original completo." email: "Mensaje original" run: "Realizar prueba" text: "Cuerpo del mensaje seleccionado" @@ -3499,11 +3522,11 @@ es: performed_by: "Realizado por" no_results: "No hay historial de moderación disponible." actions: - delete_user: "Usuario borrado" + delete_user: "Usuario eliminado" suspend_user: "Usuario suspendido" silence_user: "Usuario silenciado" - delete_post: "Publicación borrada" - delete_topic: "Tema borrado" + delete_post: "Publicación eliminada" + delete_topic: "Tema eliminado" post_approved: "Publicación aprobada" logs: title: "Registros" @@ -3538,14 +3561,14 @@ es: show: "Mostrar" modal_title: "Detalles" no_previous: "No existe un valor anterior." - deleted: "No hay un valor nuevo. El registro ha sido borrado." + deleted: "No hay un valor nuevo. El registro ha sido eliminado." actions: - delete_user: "borrar usuario" + delete_user: "eliminar usuario" change_trust_level: "cambiar nivel de confianza" change_username: "cambiar nombre de usuario" change_site_setting: "cambiar configuración del sitio" change_theme: "cambiar tema" - delete_theme: "borrar tema" + delete_theme: "eliminar tema" change_site_text: "cambiar textos del sitio" suspend_user: "suspender usuario" unsuspend_user: "desbloquear usuario" @@ -3607,7 +3630,7 @@ es: web_hook_deactivate: "desactivar webhook" embeddable_host_create: "crear host insertable" embeddable_host_update: "actualizar host insertable" - embeddable_host_destroy: "borrar host insertable" + embeddable_host_destroy: "eliminar host insertable" change_theme_setting: "cambiar la configuración del tema" disable_theme_component: "deshabilitar componente de tema" enable_theme_component: "habilitar componente de tema" @@ -3661,7 +3684,7 @@ es: show_words: "mostrar palabras" one_word_per_line: "Una palabra por línea" download: Descargar - clear_all: Borrar todo + clear_all: Eliminar todo clear_all_confirm_block: "¿Estás seguro de que quieres quitar todas las palabras agregadas a la acción «bloquear»?" clear_all_confirm_censor: "¿Estás seguro de que quieres quitar todas las palabras agregadas a la acción «censurar»?" clear_all_confirm_flag: "¿Estás seguro de que quieres quitar todas las palabras agregadas a la acción «reportar»?" @@ -3760,7 +3783,7 @@ es: cant_suspend: "Este usuario no puede ser suspendido." delete_all_posts: "Eliminar todas las publicaciones" delete_posts_progress: "Eliminando publicaciones..." - delete_posts_failed: "Hubo un problema al borrar los mensajes." + delete_posts_failed: "Hubo un problema al eliminar los mensajes." penalty_post_actions: "¿Qué te gustaría hacer con la publicación asociada?" penalty_post_delete: "Eliminar la publicación" penalty_post_delete_replies: "Eliminar la publicación+ sus respuestas" @@ -3768,7 +3791,7 @@ es: penalty_post_none: "No hacer nada" penalty_count: "Contador de faltas" clear_penalty_history: - title: "Borrar historial de faltas" + title: "Eliminado historial de faltas" description: "usuarios con faltas no pueden alcanzar NC3" delete_all_posts_confirm_MF: "Estás a punto de eliminar {POSTS, plural, one {1 post} other {# posts}} y {TOPICS, plural, one {1 topic} other {# topics}}. ¿Estás seguro?" silence: "Silenciar" @@ -3787,7 +3810,7 @@ es: logged_out: "El usuario cerró sesión en todos los dispositivos" revoke_admin: "Revocar administración" grant_admin: "Conceder administración" - grant_admin_confirm: "Te hemos enviado un correo electrónico para verificar al nuevo administrador. Por favor abre el correo y sigue las instrucciones." + grant_admin_confirm: "Te hemos enviado un correo electrónico para verificar al nuevo administrador. Por favor, abre el correo y sigue las instrucciones." revoke_moderation: "Revocar moderación" grant_moderation: "Conceder moderación" unsuspend: "Desbloquear" @@ -3823,7 +3846,7 @@ es: delete_posts_forbidden_because_staff: "No se pueden eliminar todos las publicaciones de administradores y moderadores." delete_forbidden: one: "Los usuarios no se pueden borrar si han sido registrados hace más de %{count} día, o si tienen publicaciones. Borra todas publicaciones antes de tratar de borrar un usuario." - other: "Los usuarios no se pueden eliminar si tienen publicaciones. Eliminar todas las publicaciones antes de tratar de borrar un usuario. (No se pueden eliminar las publicaciones que se hayan creado hace más de %{count} días)" + other: "Los usuarios no se pueden eliminar si tienen publicaciones. Elimina todas las publicaciones antes de tratar de eliminar a un usuario. (No se pueden eliminar las publicaciones que se hayan creado hace más de %{count} días)" cant_delete_all_posts: one: "No se pueden eliminar todos los posts. Algunos tienen más de %{count} día de antigüedad. (Ver la opción delete_user_max_post_age )" other: "No se pueden eliminar todas las publicaciones. Algunas publicaciones tienen más de %{count} días de antigüedad. (Ver la opción delete_user_max_post_age)" @@ -3948,7 +3971,7 @@ es: go_back: "Volver a la búsqueda" recommended: "Recomendamos personalizar el siguiente texto para que se ajusten a tus necesidades:" show_overriden: "Solo mostrar sobrescritos" - more_than_50_results: "Hay más de 50 resultados. Por favor afina tu busqueda." + more_than_50_results: "Hay más de 50 resultados. Por favor, afina tu busqueda." settings: show_overriden: "Solo mostrar anulados" reset: "restablecer" @@ -3963,7 +3986,7 @@ es: add_group: "añadir grupo" uploaded_image_list: label: "Editar lista" - empty: "No hay imágenes todavía. Por favor sube una." + empty: "No hay imágenes todavía. Por favor, sube una." upload: label: "Subir" title: "Subir imagen(es)" @@ -4024,11 +4047,11 @@ es: expand: Expandir … revoke_confirm: "¿Estás seguro de que quieres revocar esta insignia?" edit_badges: Editar insignias - grant_badge: Condecer insignias + grant_badge: Condecer insignia granted_badges: Insignias concedidas grant: Conceder no_user_badges: "%{name} no ha recibido ninguna insignia." - no_badges: No hay insignias para conceder. + no_badges: No hay insignias que puedan ser concedidas. none_selected: "Selecciona una insignia para empezar" allow_title: Permitir que se use la insignia como título multiple_grant: Puede ser concedida varias veces @@ -4038,7 +4061,7 @@ es: image: Imagen icon_help: "ingresa un nombre de icono de Font Awesome (usa el prefijo 'far-' para iconos regulares y 'fab-' para iconos de marca)" image_help: "Ingresa la URL de la imagen (sobrescribe el campo del icono si ambos están configurados)" - query: Consulta (SQL) para otorgar la insignia + query: Consulta de insignia (SQL) target_posts: Publicaciones destino de la consulta auto_revoke: Ejecutar diariamente la consulta de revocación show_posts: Mostrar la publicación por la cual se concedió la insignia en la página de insignias @@ -4058,7 +4081,7 @@ es: error_help: "Mira los siguientes enlaces para ayudarte con las solicitudes de las insignias" bad_count_warning: header: "¡ADVERTENCIA!" - text: "Faltan algunas muestras muestras de concesiones. Esto ocurre cuando la solicitud de la insignia devuelve ID de usuarios o de publicaciones que no existen. Esto podría causar resultados inesperados más tarde - por favor revisa de nuevo tu solicitud." + text: "Faltan algunas muestras muestras de concesiones. Esto ocurre cuando la solicitud de la insignia devuelve ID de usuarios o de publicaciones que no existen. Esto podría causar resultados inesperados más tarde - por favor, revisa de nuevo tu solicitud." no_grant_count: "No hay insignias para asignar." grant_count: one: "%{count} medalla para conceder." @@ -4143,7 +4166,7 @@ es: step: "%{current} de %{total}" upload: "Subir" uploading: "Subiendo..." - upload_error: "Lo sentimos, se produjo un error al subir este archivo. Por favor inténtalo de nuevo." + upload_error: "Lo sentimos, se produjo un error al subir este archivo. Por favor, inténtalo de nuevo." quit: "Tal vez más tarde" staff_count: one: "Tu comunidad tiene %{count} staff (tú). " diff --git a/config/locales/client.et.yml b/config/locales/client.et.yml index 25fcf02c77..bc6c5289e1 100644 --- a/config/locales/client.et.yml +++ b/config/locales/client.et.yml @@ -678,10 +678,14 @@ et: copied_to_clipboard: "Kopeeritud lõikelauale" second_factor: confirm_password_description: "Palun kinnita jätkamiseks oma parooli" + name: "Nimi" label: "Kood" rate_limit: "Palun oota enne kui proovid mõnda teist autentimise koodi." show_key_description: "Sisesta käsitsi" edit: "Muuda" + security_key: + register: "Registreeru" + delete: 'Kustuta' change_about: title: "Muuda minu andmeid" error: "Välja muutmisel tekkis viga." @@ -1058,23 +1062,18 @@ et: google_oauth2: name: "Google" title: "Google abil" - message: "Autentimine Google abil (veendu, et hüpikaknad oleks lubatud)" twitter: name: "Twitter" title: "Twitteri abil" - message: "Autentimine Twitteri abil (veendu, et hüpikaknad oleks lubatud)" instagram: name: "Instagram" title: "Instagram'iga" - message: "Autentimine Instagram'i kaudu (vaata, et hüpikaknad ei oleks keelatud)" facebook: name: "Facebook" title: "Facebooki abil" - message: "Autentimine Facebooki abil (veendu, et hüpikaknad oleks lubatud)" github: name: "GitHub" title: "GitHub abil" - message: "Autentimine GitHub abil (veendu, et hüpikaknad oleks lubatud)" invites: accept_title: "Kutse" welcome_to: "Teretulemast saidile %{site_name}!" @@ -1092,7 +1091,6 @@ et: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google klassikaline" facebook_messenger: "Facebook Messenger" @@ -2396,6 +2394,7 @@ et: user: "Kasutaja" title: "API" key: "API võti" + created: Loodud generate: "Genereeri" regenerate: "Genereeri uus" revoke: "Tühista " diff --git a/config/locales/client.fa_IR.yml b/config/locales/client.fa_IR.yml index a218afc100..93e22ec48a 100644 --- a/config/locales/client.fa_IR.yml +++ b/config/locales/client.fa_IR.yml @@ -868,7 +868,6 @@ fa_IR: copied_to_clipboard: "کپی شد" copy_to_clipboard_error: "خطا در کپی اطلاعات" remaining_codes: "شما {{count}}کد پشتیبان باقی مانده دارید." - use: "از کد پشتیبان استفاده کن" enable_prerequisites: "شما باید قبل از ساخت کد‌های پشتیبانی یک تایید هویت دوعاملی اصلی را فعال کنید " codes: title: "کد پشتیبان تولید شد" @@ -877,6 +876,7 @@ fa_IR: title: "احراز هویت دو مرحله ای" enable: "مدیریت تایید هوییت دوعاملی" confirm_password_description: "لطفا رمز عبور خود را تایید کنید تا ادامه دهیم." + name: "نام" label: "کد" rate_limit: "لطفا قبل از اینکه کد احراز هویت دیگری را تست کنید کمی صبر کنید" enable_description: | @@ -888,7 +888,6 @@ fa_IR: extended_description: | احراز هویت دو عامله امنیت بیشتری به حساب کاربری شما میدهد، چرا که یک توکن یک بار مصرف علاوه بر رمز عبور خود خواهید داشت. توکن ها در ابزارهای اندروید و IOS قابل تولید هستند. oauth_enabled_warning: "دقت کنید وقتی احراز هویت دوعامله فعال شود، ورود با حساب شبکه های اجتماعی به حساب کاربری شما از کار میفتد." - use: "از اپلیکیشن احراز هویت استفاده کنید" enforced_notice: "شما باید احراز هویت دو عامله را قبل از دسترسی به سایت فعال کنید" disable: "غیرفعال" disable_title: "غیر فعال کردن تایید دوعاملی" @@ -900,6 +899,15 @@ fa_IR: title: "تایید برپایه توکن" add: "تایید کننده جدید" default_name: "تایید‌کننده من" + security_key: + register: "ثبت‌نام" + title: 'کلیدهای امنیتی' + add: "کلید امنیتی را ثبت کنید" + default_name: "کلید امنیتی اصلی" + not_allowed_error: "روند ثبت کلید امنیتی به پایان رسیده است یا لغو شده است." + edit: 'کلید امنیتی را ویرایش کنید' + edit_description: 'نام کلید امنیتی' + delete: 'حذف' change_about: title: "تغییر «درباره‌ی من»" error: "در فرآیند تغییر این مقدار خطایی روی داد." @@ -1299,10 +1307,12 @@ fa_IR: password: "رمز‌عبور" second_factor_title: "احراز هویت دو مرحله ای" second_factor_description: "لطفا کد احراز هویت از اپ را وارد کنید:" - second_factor_backup: "ورود با کد پشتیبان" + second_factor_backup: "ورود با استفاده از یک کد پشتیبان" second_factor_backup_title: "پشتیبان دو عامله" second_factor_backup_description: "لطفا یکی از کدهای پشتیبان را وارد کنید:" - second_factor: "ورود با اپلیکیشن احراز هویت" + security_key_alternative: "نمی توانید کلید امنیتی خود را پیدا کنید یا می خواهید از روش دیگری استفاده کنید؟" + security_key_authenticate: "تأیید اعتبار با کلید امنیتی" + security_key_not_allowed_error: "روند تأیید اعتبار کلید امنیتی به پایان رسیده است یا لغو شده است." email_placeholder: "ایمیل یا نام‌کاربری" caps_lock_warning: "Caps Lock روشن است" error: "خطای ناشناخته" @@ -1335,26 +1345,22 @@ fa_IR: google_oauth2: name: "گوگل" title: "با گوگل" - message: "احراز هویت با گوگل (مطمئن شوید که بازدارنده‌های pop up فعال نباشند)" twitter: name: "توئیتر" title: "با توییتر" - message: "احراز هویت با توئیتر (مطمئن شوید که بازدارنده‌های pop up فعال نباشند)" instagram: name: "اینستاگرام" title: "با اینستاگرام" - message: "ورود با اینستاگرام (مطمئن شوید که بازدارنده‌های pop up فعال نباشند)" facebook: name: "فیس بوک" title: "با فیسبوک" - message: "اعتبارسنجی با فیسبوک (مطمئن شوید که بازدارنده‌های pop up فعال نباشند)" github: name: "گیت هاب" title: "با گیت‌هاب" - message: "اعتبارسنجی با گیت‌هاب (مطمئن شوید که بازدارنده‌های pop up فعال نباشند)" discord: name: "دیسکورد" - message: "اعتبارسنجی توسط دیسکورد" + second_factor_toggle: + backup_code: "به جای آن از یک کد پشتیبان استفاده کنید" invites: accept_title: "دعوت‌نامه" welcome_to: "به %{site_name} خوش آمدید!" @@ -1372,7 +1378,6 @@ fa_IR: apple_international: "اپل/جهانی" google: "گوگل" twitter: "توئیتر" - emoji_one: "ایموجی وان" win10: "ویندوز 10" google_classic: "گوگل کلاسیک" facebook_messenger: "پیامرسان فیس بوک" @@ -1454,6 +1459,7 @@ fa_IR: cannot_see_mention: category: "شما به {{username}} اشاره کردید ولی ایشان به دلیل نداشتن دسترسی به این دسته‌بندی، مطلع نخواهند شد. برای دسترسی باید آن‌ها را به گروهی که به این دسته‌بندی دسترسی دارند اضافه کنید." private: "شما به {{username}} اشاره کردید ولی به دلیل نداشتن دسترسی به این پیام خصوصی مطلع نخواهند شد. برای دسترسی باید آن‌ها را به این پیام خصوصی دعوت کنید." + duplicate_link: "به نظر می‌رسد پیوند شما به {{domain}} قبلاً در یک مبحث توسط @ {{username}} در پاسخ به {{ago}} ارسال شده است - مطمئن هستید که می خواهید دوباره آن را بفرستید؟" error: title_missing: "عنوان الزامی است" title_too_short: "عنوان دست‌کم باید {{min}} نویسه باشد" @@ -1831,6 +1837,7 @@ fa_IR: read_more: "می‌خواهید بیشتر بخوانید؟ {{catLink}} یا {{latestLink}}." group_request: "برای مشاهده این مبحث نیازمند درخواست عضویت در گروه `{{name}}` می‌باشید" group_join: "برای مشاهده این مبحث نیازمند پیوستن به گروه `{{name}}` می‌باشید" + group_request_sent: "درخواست عضویت شما در گروه ارسال شده است. در صورت پذیرش، شما مطلع خواهید شد." unread_indicator: "هیچ کاربری آخرین فرستهٔ این مبحث را نخوانده است." read_more_MF: "{ UNREAD, plural, =0 {} one { یک پیام خوانده نشده } other { # پیام خوانده نشده } } { NEW, plural, =0 {} one { {BOTH, select, true{و } false { } other{}} 1 موضوع جدید } other { {BOTH, select, true{و } false { } other{}} # موضوع جدید } } وجود دارد, یا {CATEGORY, select, true {نمایش سایر موضوعات دسته‌بندی {catLink}} false {{latestLink}} other {}}" browse_all_categories: جستوجوی همه‌ی دسته‌‌بندی‌ها @@ -2236,6 +2243,9 @@ fa_IR: notify_user: "یک پیام ارسال شد" bookmark: "نشانه گذاری کن" like: "پسندیده شد" + like_capped: + one: "و {{count}} نفر دیگر این را دوست داشتند" + other: "و {{count}} نفر دیگر این را پسندیده‌اند." by_you: off_topic: "شما برای این مورد پرچم خارج از بحث زدید" spam: "شما برای این مورد پرچم هرزنامه زدید" @@ -2244,6 +2254,14 @@ fa_IR: notify_user: "شما یک پیام به این کاربر ارسال کردید" bookmark: "این نوشته را نشانک زدید" like: "شما این نوشته را پسند کردید" + delete: + confirm: + one: "آیا مطمئن هستید که می خواهید آن پست را حذف کنید؟" + other: "آیا مطمئن هستید که می‌خواهید آن {{count}} فرسته را حذف کنید؟" + merge: + confirm: + one: "آیا مطمئن هستید که می خواهید آن پست ها را ادغام کنید؟" + other: "آیا مطمئن هستید که می‌خواهید آن {{count}} فرسته را ادغام کنید؟" revisions: controls: first: "بازبینی نخست" @@ -2918,6 +2936,7 @@ fa_IR: user: "کاربر" title: "API" key: "کلید API" + created: ساخته شده generate: "تولید کردن" regenerate: "ایجاد مجدد" revoke: "ابطال" diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index 3483da96f4..f5035515b0 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -834,13 +834,13 @@ fi: copied_to_clipboard: "Kopioitiin leikepöydälle" copy_to_clipboard_error: "Virhe kopioimisessa leikepöydälle" remaining_codes: "Sinulla on {{count}} varakoodia jäljellä." - use: "Käytä varakoodi" codes: title: "Varakoodit luotiin" description: "Jokaista näistä varakoodeista voi käyttää vain kerran. Säilö ne johonkin turvalliseen paikkaan, mistä kuitenkin löydät ne." second_factor: title: "Kaksivaiheinen tunnistautuminen" confirm_password_description: "Jatka vahvistamalla salasanasi" + name: "Nimi" label: "Koodi" rate_limit: "Odota hetki ennen toisen todennuskoodin tarjoamista." enable_description: | @@ -852,9 +852,11 @@ fi: extended_description: | Kaksivaiheinen tunnistautuminen lisää tilisi tietoturvaa vaatimalla kertakäyttöisen koodin salasanan lisäksi. Koodeja voi luoda Android- ja iOS-laitteilla. oauth_enabled_warning: "Huomioi, ettet voi kirjautua some-tilien avulla, jos kaksivaiheinen tunnistautuminen on käytössä." - use: "Käytä Authenticator-sovellusta" enforced_notice: "Kaksivaiheinen tunnistautuminen pitää ottaa käyttöön, jotta voi käyttää sivustoa." edit: "Muokkaa" + security_key: + register: "Rekisteröidy" + delete: 'Poista' change_about: title: "Muokkaa kuvaustasi" error: "Arvon muuttamisessa tapahtui virhe." @@ -1242,10 +1244,8 @@ fi: password: "Salasana" second_factor_title: "Kaksivaiheinen tunnistautuminen" second_factor_description: "Syötä sovelluksen antama todennuskoodi:" - second_factor_backup: "Kirjaudu varakoodilla" second_factor_backup_title: "Varakoodit" second_factor_backup_description: "Syötä yksi varakoodeistasi:" - second_factor: "Kirjaudu Authenticator-sovelluksella" email_placeholder: "sähköposti tai käyttäjätunnus" caps_lock_warning: "Caps Lock on päällä" error: "Tuntematon virhe" @@ -1278,25 +1278,23 @@ fi: google_oauth2: name: "Google" title: "Googlella" - message: "Todennetaan Googlen kautta (varmista, että ponnahdusikkunoiden esto ei ole päällä)" twitter: name: "Twitter" title: "Twitterillä" - message: "Todennetaan Twitterin kautta (varmista, että ponnahdusikkunoiden esto ei ole päällä)" instagram: name: "Instagram" title: "Instagramilla" - message: "Todennetaan Instagramin kautta (varmista, että ponnahdusikkunoiden esto ei ole päällä)" facebook: name: "Facebook" title: "Facebookilla" - message: "Todennetaan Facebookin kautta (varmista, että ponnahdusikkunoiden esto ei ole päällä)" github: name: "GitHub" title: "GitHubilla" - message: "Todennetaan Githubin kautta (varmista, että ponnahdusikkunoiden esto ei ole päällä)" discord: name: "Discord" + second_factor_toggle: + totp: "Käytä todennussovellusta tämän sijaan" + backup_code: "Käytä varmuuskoodia tämän sijaan" invites: accept_title: "Kutsu" welcome_to: "Tervetuloa sivustolle %{site_name}!" @@ -1314,7 +1312,6 @@ fi: apple_international: "Apple/Kansainvälinen" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -2920,6 +2917,7 @@ fi: user: "Käyttäjä" title: "Rajapinta" key: "Rajapinnan avain" + created: Luotu generate: "Luo" regenerate: "Tee uusi" revoke: "Peruuta" @@ -3234,7 +3232,6 @@ fi: other: "Teema on {{count}} muutosta perässä!" compare_commits: "(Näytä uudet muutokset)" repo_unreachable: "Teeman Git-tietovarastoon yhdistäminen epäonnistui. Virheviesti:" - imported_from_archive: "Teema tuotiin .tar.gz-tiedostosta" scss: text: "CSS" title: "Lisää mukautettua CSS:ää, hyväksymme käyvät CSS- ja SCSS-tyylit" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 183c9a531c..e98adf0f36 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -858,7 +858,6 @@ fr: copied_to_clipboard: "Copié dans le presse-papiers" copy_to_clipboard_error: "Erreur en copiant les données dans le presse-papiers" remaining_codes: "Vous avez {{count}} codes de secours restants." - use: "Utiliser un code de sauvegarde" enable_prerequisites: "Vous devez activer un second facteur principal avant de générer des codes de sauvegarde." codes: title: "Codes de secours générés" @@ -867,6 +866,7 @@ fr: title: "Authentification à deux facteurs" enable: "Paramètres d'authentification à deux facteurs" confirm_password_description: "Merci de confirmer votre mot de passe pour continuer" + name: "Nom" label: "Code" rate_limit: "Veuillez patienter avant d'essayer un autre code d'identification." enable_description: | @@ -877,7 +877,6 @@ fr: Protéger votre compte avec des codes de sécurité à usage unique. extended_description: "L'authentification à deux facteurs ajoute une sécurité supplémentaire à votre compte en exigeant un jeton unique en \nplus de votre mot de passe. Les jetons peuvent être générés sur les appareils Android et iOS.\n" oauth_enabled_warning: "Veuillez noter que les connexions sociales seront désactivées une fois que l'authentification à deux facteurs aura été activée sur votre compte." - use: "Utiliser l'application Authenticator" enforced_notice: "Vous devez activer l'authentification à deux facteurs pour accéder à ce site." disable: "désactiver" disable_title: "Désactiver le second facteur" @@ -889,6 +888,9 @@ fr: title: "Authentificateurs à base de jetons" add: "Nouveau Authentificateur" default_name: "Mon Authentificateur" + security_key: + register: "Créer" + delete: 'Supprimer' change_about: title: "Modifier À propos de moi" error: "Il y a eu une erreur lors de la modification de cette valeur." @@ -1288,10 +1290,8 @@ fr: password: "Mot de passe" second_factor_title: "Authentification à deux facteurs" second_factor_description: "Veuillez saisir le code d'authentification de votre app :" - second_factor_backup: "Se connecter avec un code de secours" second_factor_backup_title: "Authentification à deux facteurs (code de secours)" second_factor_backup_description: "Veuillez entrer un de vos codes de secours :" - second_factor: "Se connecter avec une application" email_placeholder: "courriel ou pseudo" caps_lock_warning: "Majuscules vérrouillées" error: "Erreur inconnue" @@ -1324,25 +1324,23 @@ fr: google_oauth2: name: "Google" title: "via Google" - message: "Authentification via Google (assurez-vous que les popups ne soient pas bloquées)" twitter: name: "Twitter" title: "via Twitter" - message: "Authentification via Twitter (assurez-vous que les popups ne soient pas bloquées)" instagram: name: "Instagram" title: "via Instagram" - message: "Authentification via Instagtram (assurez-vous que les popups ne soient pas bloquées)" facebook: name: "Facebook" title: "via Facebook" - message: "Authentification via Facebook (assurez-vous que les popups ne soient pas bloquées)" github: name: "GitHub" title: "via GitHub" - message: "Authentification via GitHub (assurez-vous que les popups ne soient pas bloquées)" discord: name: "Discord" + second_factor_toggle: + totp: "Utilisez plutôt une application d'authentification" + backup_code: "Utilisez plutôt un code de secours" invites: accept_title: "Invitation" welcome_to: "Bienvenue sur %{site_name} !" @@ -1360,7 +1358,6 @@ fr: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Classique Google" facebook_messenger: "Facebook Messenger" @@ -3007,6 +3004,7 @@ fr: user: "Utilisateur" title: "API" key: "Clé API" + created: Créé generate: "Générer" regenerate: "Regénérer" revoke: "Révoquer" @@ -3329,7 +3327,6 @@ fr: other: "Le thème est en retard de {{count}} commits !" compare_commits: "(Voir les nouveaux changements)" repo_unreachable: "Le dépôt Git de ce thème reste inaccessible. Message d'erreur : " - imported_from_archive: "Ce thème a été importé d'un fichier .tar.gz" scss: text: "CSS" title: "Entrez du CSS personnalisé, nous acceptons tous les styles CSS et SCSS valides." diff --git a/config/locales/client.gl.yml b/config/locales/client.gl.yml index eb047a48b0..23a141ca81 100644 --- a/config/locales/client.gl.yml +++ b/config/locales/client.gl.yml @@ -509,7 +509,10 @@ gl: disable: "Desactivar" enable: "Activar" second_factor: + name: "Nome" edit: "Editar" + security_key: + delete: 'Eliminar' change_about: title: "Cambiar «Verbo de min»" change_username: @@ -808,20 +811,15 @@ gl: google_oauth2: name: "Google" title: "co Google" - message: "Autenticación mediante Google (asegúrate de ter desactivado o bloqueador de xanelas emerxentes)" twitter: name: "Twitter" title: "co Twitter" - message: "Autenticación mediante Twitter (asegúrate de ter desactivado o bloqueador de xanelas emerxentes)" instagram: title: "con Instagram" - message: "Autenticación con Instagram (asegúrate que os bloqueadores de publicidade estean desactivados)" facebook: title: "co Facebook" - message: "Autenticación mediante Facebook (asegúrate de ter desactivado o bloqueador de xanelas emerxentes)" github: title: "co GitHub" - message: "Autenticación mediante Github (asegúrate de ter desactivado o bloqueador de xanelas emerxentes)" invites: welcome_to: "Benvido/a a %{site_name}!" name_label: "Nome" @@ -832,7 +830,6 @@ gl: apple_international: "Apple/Internacional" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" shortcut_modifier_key: shift: "Maiús." ctrl: "Ctrl" @@ -1781,6 +1778,7 @@ gl: user: "Usuario" title: "API" key: "Chave da API" + created: Creado generate: "Xerar" regenerate: "Rexenerar" revoke: "Revogar" diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index 3cf86aa31b..5c02a5c9a0 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -960,7 +960,7 @@ he: copied_to_clipboard: "הועתק ללוח" copy_to_clipboard_error: "שגיאה בהעתקת מידע ללוח" remaining_codes: "יש לך {{count}} קודי גיבוי נותרים" - use: "שימוש בקוד גיבוי" + use: "להשתמש בקוד גיבוי" enable_prerequisites: "עליך להפעיל אימות דו־שלבי עיקרי בטרם יצירת קודים כגיבוי." codes: title: "קודי גיבוי נוצרו" @@ -968,7 +968,9 @@ he: second_factor: title: "אימות ב2 גורמים" enable: "ניהול אימות דו־שלבי" + forgot_password: "שכחת את הססמה?" confirm_password_description: "אנא אשר את סיסמתך בכדי להמשיך" + name: "שם" label: "קוד" rate_limit: "אנא המתינו לפני שתנסו קוד אישור אחר." enable_description: | @@ -980,7 +982,7 @@ he: extended_description: | אימות ב2 גורמים מחזק את אבטחת המשתמש שלך על ידי אסימון אבטחה חד-פעמי בנוסף לסיסמה שלך. ניתן ליצור אסימונים על מכשיריAndroid וiOS. oauth_enabled_warning: "לידיעתך, כניסות מרשתות חברתיות ינוטרלו לאחר הפעלת אימות בשני שלבים בחשבונך." - use: "להשתמש ביישומון אימות" + use: "להשתמש ביישומון אימות" enforced_notice: "עליך להפעיל אימות דו־שלבי בטרם הגישה לאתר הזה." disable: "נטרול" disable_title: "נטרול אימות דו־שלבי" @@ -988,10 +990,21 @@ he: edit: "ערוך" edit_title: "עריכת אימות דו־שלבי" edit_description: "שם אימות דו־שלבי" + enable_security_key_description: "כשמפתח האבטחה הפיזי שלך מוכן יש ללחוץ על כפתור הרישום שלהלן." totp: title: "מאמתים מבוססי אסימונים" add: "מאמת חדש" default_name: "המאמת שלי" + security_key: + register: "הרשמה" + title: 'מפתחות אבטחה' + add: "רישום מפתח אבטחה" + default_name: "מפתח אבטחה עיקרי" + not_allowed_error: "זמן תהליך רישום מפתח האבטחה פג או שבוטל." + already_added_error: "כבר רשמת את מפתח האבטחה הזה. אין צורך לרשום אותו שוב." + edit: 'עריכת מפתח אבטחה' + edit_description: 'שם מפתח אבטחה' + delete: 'מחיקה' change_about: title: "שינוי בנוגע אליי" error: "ארעה שגיאה בשינוי ערך זה." @@ -1416,10 +1429,16 @@ he: password: "סיסמה" second_factor_title: "אימות ב2 גורמים" second_factor_description: "אנא הכניסו את קוד האישור מהיישומון שלכם." - second_factor_backup: "כניסה באמצעות קוד גיבוי" + second_factor_backup: "כניסה עם קוד גיבוי" second_factor_backup_title: "גיבוי דו־שלבי" second_factor_backup_description: "נא להקליד אחד מהקודים לגיבוי שלך:" - second_factor: "כניסה באמצעות יישומון אימות" + second_factor: "כניסה עם יישומון אימות" + security_key_description: "כשמפתח האבטחה הפיזי שלך מוכן יש ללחוץ על כפתור האימות עם מפתח האבטחה שלהלן." + security_key_alternative: "לא הצלחת למצוא את מפתח האבטחה או שברצונך לנסות שיטה אחרת?" + security_key_authenticate: "אימות עם מפתח אבטחה" + security_key_not_allowed_error: "זמן תהליך אימות מפתח האבטחה פג או שבוטל." + security_key_no_matching_credential_error: "לא ניתן למצוא פרטי גישה במפתח האבטחה שסופק." + security_key_support_missing_error: "המכשיר או הדפדפן הנוכחי שלך לא תומך בשימוש במפתחות אבטחה, נא להשתמש בשיטה אחרת." email_placeholder: "דואר אלקטרוני או שם משתמש/ת" caps_lock_warning: "מקש Caps Lock לחוץ" error: "שגיאה לא ידועה" @@ -1452,27 +1471,24 @@ he: google_oauth2: name: "Google" title: "עם Google" - message: "אימות עם Google (יש לוודא שחוסם החלונות הקופצים אינו פעיל)" twitter: name: "Twitter" title: "עם Twitter" - message: "אימות עם Twitter (יש לוודא שחוסם חלונות קופצים אינו פעיל)" instagram: name: "Instagram" title: "עם אינסטגרם" - message: "אימות עם אינסטגרם (יש לוודא שחוסם חלונות קופצים אינו פעיל)" facebook: name: "Facebook" title: "עם Facebook" - message: "אימות עם Facebook (יש לוודא שחוסם חלונות קופצים אינו פעיל)" github: name: "GitHub" title: "עם GitHub" - message: "אימות עם GitHub (יש לוודא שחוסם חלונות קופצים אינו פעיל)" discord: name: "Discord" title: "עם Discord" - message: "אימות עם Discord" + second_factor_toggle: + totp: "להשתמש ביישומון אימות במקום" + backup_code: "להשתמש בקוד גיבוי במקום" invites: accept_title: "הזמנה" welcome_to: "ברוך בואך אל %{site_name}!" @@ -1490,7 +1506,7 @@ he: apple_international: "אפל/בינלאומי" google: "גוגל" twitter: "טוויטר" - emoji_one: "Emoji One" + emoji_one: "JoyPixels (לשעבר EmojiOne)" win10: "חלונות 10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -3255,6 +3271,9 @@ he: user: "משתמש" title: "API" key: "מפתח API" + created: נוצר + last_used: שימוש אחרון + never_used: (אף פעם) generate: "ייצר" regenerate: "ייצר מחדש" revoke: "שלול" @@ -3583,7 +3602,7 @@ he: other: "ערכת העיצוב מעוכבת ב־{{count}} הגשות!" compare_commits: "(צפייה בהגשות חדשות)" repo_unreachable: "לא ניתן ליצור קשר עם מאגר ה־git של ערכת העיצוב הזו. הודעת השגיאה:" - imported_from_archive: "ערכת עיצוב זו ייובאה מקובץ ‎.tar.gz" + imported_from_archive: "ערכת עיצוב זו ייובאה מקובץ ‎.zip" scss: text: "CSS" title: "עריכת CSS מותאם, אנחנו מקבלים כל סגנון CSS ו SCSS תקני" diff --git a/config/locales/client.hu.yml b/config/locales/client.hu.yml index 1e5957588c..4552cbee44 100644 --- a/config/locales/client.hu.yml +++ b/config/locales/client.hu.yml @@ -856,11 +856,15 @@ hu: second_factor: title: "Két-faktoros hitelesítés" confirm_password_description: "Kérlek erősítsd meg a jelszavad a továbbhaladáshoz" + name: "Név" label: "Kód" disable_description: "Írd be az azonosító kódodat az alkalmazásból" show_key_description: "Manuális beírás" oauth_enabled_warning: "A közösségi bejelentkezések nem lesznek elérhetőek a fét-faktoros azonosítás aktiválása után a fiókhoz." edit: "Szerkesztés" + security_key: + register: "Regisztráció" + delete: 'Törlés' change_about: title: "Rólam megváltoztatása" error: "Hiba történt az adat módosításakor." @@ -1215,9 +1219,7 @@ hu: password: "Jelszó" second_factor_title: "Kétlépcsős azonosítás" second_factor_description: "Kérlek írd be az azonosítási kódodat az alkalmazásodból" - second_factor_backup: "Jelentkezz be a biztonsági kód használatával" second_factor_backup_description: "Kérlek írd be valamelyik biztonsági kódodat" - second_factor: "Jelentkezz be Hitelesítő alkalmazás használatával" email_placeholder: "e-mail vagy felhasználónév" caps_lock_warning: "A Caps Lock be van kapcsolva" error: "Ismeretlen hiba" @@ -1248,23 +1250,18 @@ hu: google_oauth2: name: "Google" title: "Google" - message: "Azonosítás a Google-lel (ellenőrizd, hogy a felugró ablakok engedélyezve vannak)" twitter: name: "Twitter" title: "Twitter" - message: "Azonosítás a Twitter-rel (ellenőrizd, hogy a felugró ablakok engedélyezve vannak)" instagram: name: "Instagram" title: "Instagram" - message: "Azonosítás Instagrammal (győződj meg róla hogy a felugró ablakok nincsenek engedélyezve)" facebook: name: "Facebook" title: "Facebook" - message: "Azonosítás a Facebook-kal (ellenőrizd, hogy a felugró ablakok engedélyezve vannak)" github: name: "GitHub" title: "GitHub" - message: "Azonosítás a GitHub-bal (ellenőrizd, hogy a felugró ablakok engedélyezve vannak)" discord: name: "Discord" invites: @@ -1283,7 +1280,6 @@ hu: apple_international: "Apple/Nemzetközi" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -2471,6 +2467,7 @@ hu: user: "Felhasználó" title: "API" key: "API kulcs" + created: Létrehozott generate: "Generálás" regenerate: "Regenerálás" revoke: "Visszavonás" diff --git a/config/locales/client.hy.yml b/config/locales/client.hy.yml index 044d1452d2..c8cba23c88 100644 --- a/config/locales/client.hy.yml +++ b/config/locales/client.hy.yml @@ -756,13 +756,13 @@ hy: copied_to_clipboard: "Կրնօրինակված է Փոխանակման հարթակում" copy_to_clipboard_error: "Փոխանակման հարթակում տվյալների կրկնօրինակման սխալ" remaining_codes: "Ձեզ մնացել է պահուստային {{count}} կոդ:" - use: "Օգտագործեք պահուստի կոդ" codes: title: "Պահուստային Կոդերը Գեներացվել են" description: "Այս պահուստային կոդերից յուրաքանչյուրը կարող է օգտագործվել միայն մեկ անգամ: Պահեք դրանք ապահով, բայց հասանելի վայրում:" second_factor: title: "Երկգործոն վավերացում" confirm_password_description: "Շարունակելու համար խնդրում ենք հաստատել Ձեր գաղտնաբառը" + name: "Անուն" label: "Կոդ" rate_limit: "Խնդրում ենք սպասել՝ նախքան մեկ այլ վավերացման կոդ փորձելը:" enable_description: | @@ -772,9 +772,11 @@ hy: extended_description: | Երկգործոն վավերացումն ավելացնում է էքստրա-անվտանգություն Ձեր հաշվին՝ պահանջելով մեկանգամյա կոդանշան(token)՝ ի հավելումն Ձեր գաղտնաբառին: Կոդանշանները կարող են գեներացվել Android և iOS սարքերով: oauth_enabled_warning: "Խնդրում ենք նկատի ունենալ, որ սոցիալական ցանցերով մուտքը կանջատվի, հենց որ երկգործոն վավերացումը միացվի Ձեր հաշվի համար:" - use: "Օգտագործեք Authenticator հավելվածը" enforced_notice: "Դուք պարտավոր եք միացնել երկգործոն նույնականացումը՝ մինչ մուտք գործելը այս կայք:" edit: "Խմբագրել" + security_key: + register: "Գրանցվել" + delete: 'Ջնջել' change_about: title: "Փոփոխել Իմ Մասին բաժինը" error: "Այս արժեքը փոփոխելիս տեղի է ունեցել սխալ:" @@ -1158,10 +1160,8 @@ hy: password: "Գաղտնաբառ" second_factor_title: "Երկգործոն Վավերացում" second_factor_description: "Խնդրում ենք Ձեր հավելվածից մուտքագրել վավերացման կոդը՝" - second_factor_backup: "Մուտք գործել օգտագործելով պահուստային կոդ" second_factor_backup_title: "Երկգործոն Պահուստային Պատճենում (Two Factor Backup)" second_factor_backup_description: "Խնդրում ենք մուտքագրել ձեր պահուստային կոդերից որևէ մեկը՝" - second_factor: "Մուտք գործել օգտագործելով Authenticator հավելվածը" email_placeholder: "էլ. հասցե կամ օգտանուն" caps_lock_warning: "Caps Lock-ը միացված է" error: "Անհայտ սխալ" @@ -1193,25 +1193,23 @@ hy: google_oauth2: name: "Google" title: "Google-ով" - message: "Վավերացում Google-ով (համոզվեք, որ փոփ-ափ արգելափակումները միացված չեն)" twitter: name: "Twitter" title: "Twitter-ով" - message: "Վավերացում Twitter-ով (համոզվեք, որ փոփ-ափ արգելափակումները միացված չեն)" instagram: name: "Instagram" title: "Instagram-ով" - message: "Վավերացում Instagram-ով (համոզվեք, որ փոփ-ափ արգելափակումները միացված չեն)" facebook: name: "Facebook" title: "Facebook-ով" - message: "Վավերացում Facebook-ով (համոզվեք, որ փոփ-ափ արգելափակումները միացված չեն)" github: name: "GitHub" title: "GitHub-ով" - message: "Վավերացում GitHub-ով (համոզվեք, որ փոփ-ափ արգելափակումները միացված չեն)" discord: name: "Discord" + second_factor_toggle: + totp: "Փոխարենը օգտագործել նույնականացման հավելվածը" + backup_code: "Փոխարենը օգտագործել պահուստային կոդը" invites: accept_title: "Հրավեր" welcome_to: "Բարի Գալուստ %{site_name}!" @@ -1229,7 +1227,6 @@ hy: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -2773,6 +2770,7 @@ hy: user: "Օգտատեր" title: "API" key: "API Key" + created: Ստեղծված generate: "Գեներացնել" regenerate: "Վերագեներացնել" revoke: "Հետ կանչել" @@ -3081,7 +3079,6 @@ hy: other: "Թեման {{count}} commit ետ է!" compare_commits: "(Տեսնել նոր commit-ները)" repo_unreachable: "Հնարավոր չէ կապ հաստատել այս թեմայի Git ռեպոզիտորիայի հետ: Սխալի հաղորդագրությունը՝ " - imported_from_archive: "Այս թեման ներմուծվել է .tar.gz ֆայլից" scss: text: "CSS" title: "Մուտքագրեք մասնավոր CSS, մենք ընդունում ենք բոլոր վավեր CSS և SCSS ոճերը" diff --git a/config/locales/client.id.yml b/config/locales/client.id.yml index 824489c5ad..bc072de8c4 100644 --- a/config/locales/client.id.yml +++ b/config/locales/client.id.yml @@ -559,9 +559,12 @@ id: disable: "Nonaktifkan" enable: "Aktifkan" second_factor: + name: "Nama" label: "Kode" show_key_description: "Masukkan secara manual" edit: "Ubah" + security_key: + delete: 'Hapus' change_about: title: "Ganti Tentang Saya" error: "Ada kesalahan saat mengganti nilai ini" @@ -820,19 +823,14 @@ id: submit_new_email: "Perbarui Alamat Email" google_oauth2: title: "dengan Google" - message: "Autentikasi dengan Google (pastikan pop up blockers tidak dinyalakan)" twitter: title: "dengan Twitter" - message: "Autentikasi dengan Twitter (pastikan pop up blockers tidak dinyalakan)" instagram: title: "dengan Instagram" - message: "Autentikasi dengan Instagram (pastikan pop up blockers tidak dinyalakan)" facebook: title: "dengan Facebook" - message: "Autentikasi dengan Facebook (pastikan pop up blockers tidak dinyalakan)" github: title: "dengan GitHub" - message: "Autentikasi dengan GitHub (pastikan pop up blockers tidak dinyalakan)" invites: accept_title: "Undangan" welcome_to: "Selamat datang di %{site_name}!" @@ -1156,6 +1154,7 @@ id: automatic: "Otomatis" api: user: "Pengguna" + created: Dibuat web_hooks: destroy: "Hapus" active: "Aktif" diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index 36bcfc0897..42712d12c5 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -850,13 +850,13 @@ it: copied_to_clipboard: "Copiato nella Clipboard" copy_to_clipboard_error: "Errore durante la copia nella Clipboard" remaining_codes: "Ti sono rimasti {{count}} codici di backup." - use: "Usa un codice di backup" codes: title: "Codici di backup generati" description: "Ciascuno di questi codici di backup può essere usato una sola volta. Conservali in un posto sicuro ma accessibile." second_factor: title: "Autenticazione a Due Fattori" confirm_password_description: "Per favore conferma la tua password per continuare" + name: "Nome" label: "Codice" rate_limit: "Per favore, attendi prima di provare un altro codice di autenticazione." enable_description: | @@ -868,9 +868,11 @@ it: extended_description: | L'autenticazione a due fattori aggiunge ulteriore sicurezza al tuo account attraverso la richiesta di un token usa e getta oltre alla tua password. I token possono essere generati su dispositivi Android e iOS . oauth_enabled_warning: "Tieni presente che gli accessi ai social network saranno disabilitati dopo aver attivato l'autenticazione a due fattori nel tuo account." - use: "Usa l'applicazione Authenticator " enforced_notice: "E' obbligatorio abilitare l'autenticazione a due fattori per accedere a questo sito." edit: "Modifica" + security_key: + register: "Registrare" + delete: 'Elimina' change_about: title: "Modifica i dati personali" error: "Si è verificato un errore durante la modifica del valore." @@ -1265,10 +1267,8 @@ it: password: "Password" second_factor_title: "Autenticazione a due fattori" second_factor_description: "Per favore, inserisci il codice di autenticazione della tua app:" - second_factor_backup: "Connessione tramite codice di backup" second_factor_backup_title: "Backup Due Fattori" second_factor_backup_description: "Per favore, inserisci uno dei tuoi codici di backup:" - second_factor: "Connessione tramite l'app Authenticator" email_placeholder: "email o nome utente" caps_lock_warning: "Il Blocco Maiuscole è attivo" error: "Errore sconosciuto" @@ -1301,25 +1301,23 @@ it: google_oauth2: name: "Google" title: "con Google" - message: "Autenticazione tramite Google (assicurati che il blocco pop up non siano attivo)" twitter: name: "Twitter" title: "con Twitter" - message: "Autenticazione con Twitter (assicurati che il blocco pop up non sia attivo)" instagram: name: "Instagram" title: "con Instagram" - message: "Autenticazione con Instagram (assicurati che il blocco pop up non sia attivo)" facebook: name: "Facebook" title: "con Facebook" - message: "Autenticazione con Facebook (assicurati che il blocco pop up non sia attivo)" github: name: "GitHub" title: "con GitHub" - message: "Autenticazione con GitHub (assicurati che il blocco pop up non sia attivo)" discord: name: "Discord" + second_factor_toggle: + totp: "Utilizzare un'app di autenticazione" + backup_code: "Utilizza un codice di backup" invites: accept_title: "Invito" welcome_to: "Benvenuto su %{site_name}!" @@ -1337,7 +1335,6 @@ it: apple_international: "Apple/Internazionale" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -2940,6 +2937,7 @@ it: user: "Utente" title: "API" key: "Chiave API" + created: Creazione generate: "Genera" regenerate: "Rigenera" revoke: "Revoca" @@ -3255,7 +3253,6 @@ it: other: "Il tema è indietro di {{count}} aggiornamenti!" compare_commits: "(Vedi i nuovi commit)" repo_unreachable: "Impossibile accedere al Git repository di questo tema. Messaggio di errore:" - imported_from_archive: "Questo tema è stato importato da un file .tar.gz" scss: text: "CSS" title: "Inserire il CSS personalizzato, accettiamo tutti gli stili validi di CSS e SCSSpersonalizzato" diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index 8f3e07f4d5..b488478714 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -758,17 +758,19 @@ ja: copied_to_clipboard: "クリップボードにコピーしました" copy_to_clipboard_error: "クリップボードにコピーする際にエラーが発生しました" remaining_codes: "{{count}} 個のバックアップコードが残っています。" - use: "バックアップコードを使用" codes: title: "バックアップコードが作られました" second_factor: title: "2段階認証" confirm_password_description: "続行するにはパスワードを入力してください" + name: "名前" label: "コード" show_key_description: "手動入力" oauth_enabled_warning: "アカウントで2段階認証が有効になると、ソーシャルログインは無効になります。" enforced_notice: "このサイトにアクセスするには2段階認証を有効にする必要があります。" edit: "編集" + security_key: + delete: '削除する' change_about: title: "プロフィールを変更" error: "変更中にエラーが発生しました。" @@ -1154,23 +1156,18 @@ ja: google_oauth2: name: "Google" title: "Google" - message: "Googleによる認証 (ポップアップがブロックされていないことを確認してください)" twitter: name: "Twitter" title: "Twitter" - message: "Twitter による認証 (ポップアップがブロックされていないことを確認してください)" instagram: name: "Instagram" title: "Instagram" - message: "Instagram による認証 (ポップアップがブロックされていないことを確認してください)" facebook: name: "Facebook" title: "Facebook" - message: "Facebook による認証 (ポップアップがブロックされていないことを確認してください)" github: name: "GitHub" title: "GitHub" - message: "Github による認証 (ポップアップがブロックされていないことを確認してください)" invites: accept_title: "招待" welcome_to: "%{site_name}へようこそ!" @@ -1188,7 +1185,6 @@ ja: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebookメッセンジャー" @@ -2430,6 +2426,7 @@ ja: user: "ユーザ" title: "API" key: "Key" + created: 作成者 generate: "API キーを生成" regenerate: "API キーを再生成" revoke: "無効化" diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index 8faa0b99d3..7d1afdb579 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -732,9 +732,12 @@ ko: second_factor: title: "이중 인증" confirm_password_description: "비밀번호를 확인해주세요." + name: "이름" label: "코드" - use: "OTP 앱 사용" edit: "수정" + security_key: + register: "등록하기" + delete: '삭제' change_about: title: "내 소개 변경" error: "값을 바꾸는 중 에러가 발생했습니다." @@ -1071,7 +1074,6 @@ ko: username: "아이디" password: "비밀번호" second_factor_title: "이중 인증" - second_factor_backup: "백업 코드를 사용해 로그인" second_factor_backup_description: "백업 코드 중 하나를 입력하세요:" email_placeholder: "이메일 주소 또는 아이디" caps_lock_warning: "Caps Lock 켜짐" @@ -1101,23 +1103,20 @@ ko: google_oauth2: name: "구글" title: "with Google" - message: "구글을 통해 인증 중 (파업이 허용되어 있는지 확인해주세요.)" twitter: name: "트위터" title: "with Twitter" - message: "Twitter 인증 중(팝업 차단을 해제 하세요)" instagram: name: "인스타그램" title: "인스타그램" - message: "인스타그램으로 인증 중 (팝업이 허용되어 있는지 확인해주세요)" facebook: name: "페이스북" title: "with Facebook" - message: "Facebook 인증 중(팝업 차단을 해제 하세요)" github: name: "GitHub" title: "GitHub" - message: "GitHub 인증 중(팝업 차단을 해제 하세요)" + second_factor_toggle: + backup_code: "대신 백업 코드 사용" invites: accept_title: "초대장" welcome_to: "%{site_name}에 오신것을 환영합니다." @@ -1135,7 +1134,6 @@ ko: apple_international: "Apple/International" google: "구글" twitter: "트위터" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google 클래식" facebook_messenger: "Facebook 메신저" @@ -2439,6 +2437,7 @@ ko: user: "사용자" title: "API" key: "API 키" + created: 생성일자 generate: "API 키 생성" regenerate: "API 키 재생성" revoke: "폐지" diff --git a/config/locales/client.lt.yml b/config/locales/client.lt.yml index a79f0495ab..fb9d5d388e 100644 --- a/config/locales/client.lt.yml +++ b/config/locales/client.lt.yml @@ -732,9 +732,13 @@ lt: second_factor: title: "Dviejų veiksmų autentifikavimas" confirm_password_description: "Patvirtinkite slaptažodį norėdami tęsti" + name: "Vardas" label: "Kodas" show_key_description: "Įveskite rankiniu būdu" edit: "Redaguoti" + security_key: + register: "Registruokis" + delete: 'Pašalinti' change_about: title: "Keisti Apie Mane" error: "Rasta klaida bandant pakeisti reikšmę. Prašom patikslinti veiksmus." @@ -1052,7 +1056,6 @@ lt: username: "Vartotojas" password: "Slaptažodis" second_factor_title: "Dviejų veiksmų autentifikavimas" - second_factor_backup: "Prisijunti naudojant atsarginį kodą" email_placeholder: "el. paštas arba slaptažodis" caps_lock_warning: "Įjungtas didžiųjų raidžių rašymas" error: "Nežinoma klaida" @@ -1077,23 +1080,18 @@ lt: google_oauth2: name: "Google" title: "per Google" - message: "Prisijungiama per Google (įsitikinkite, kad neblokuojate iššokamų langų)" twitter: name: "Twitter" title: "per Twitter" - message: "Prisijungiama per Twitter (įsitikinkite, kad neblokuojate iššokamų langų)" instagram: name: "Instagram" title: "su Instagram" - message: "Prisijungiama per Instagram (įsitikinkite, kad neblokuojate iššokamų langų)" facebook: name: "Facebook" title: "per Facebook" - message: "Prisijungiama per Facebook (įsitikinkite, kad neblokuojate iššokamų langų)" github: name: "GitHub" title: "per GitHub" - message: "Prisijungiama per GitHub (įsitikinkite, kad neblokuojate iššokamų langų)" invites: welcome_to: "Sveiki atvykę į %{site_name}!" invited_by: "Jus pakvietė:" @@ -1107,7 +1105,6 @@ lt: apple_international: "Apple" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Žinutės" @@ -2426,6 +2423,7 @@ lt: user: "Vartotojas" title: "API" key: "API raktas" + created: Sukurta generate: "Generuoti" regenerate: "Pergeneruoti" revoke: "Pašalinti" diff --git a/config/locales/client.lv.yml b/config/locales/client.lv.yml index bc8595ba58..0b28d1cf28 100644 --- a/config/locales/client.lv.yml +++ b/config/locales/client.lv.yml @@ -646,7 +646,10 @@ lv: copied_to_clipboard: "Nokopēts uz starpliktuvi (clipboard)" copy_to_clipboard_error: "Radās kļūda, kopējot uz starpliktuvi (clipboard)" second_factor: + name: "Vārds" edit: "Labot" + security_key: + delete: 'Dzēst' change_about: title: "Mainīt aprakstu par mani" error: "Mainot šo vērtību, notika kļūda." @@ -1002,20 +1005,15 @@ lv: google_oauth2: name: "Google" title: "ar Google" - message: "Autorizēšanās ar Google (pārliecinieties, ka nav bloķēti uznirstošie logi)" twitter: name: "Twitter" title: "ar Twitter" - message: "Autorizēšanās ar Twitter (pārliecinieties, ka nav bloķēti uznirstošie logi)" instagram: title: "ar Instagram" - message: "Autorizēšanās ar Instagram (pārliecinieties, ka nav bloķēti uznirstošie logi)" facebook: title: "ar Facebook" - message: "Autorizēšanās ar Facebook (pārliecinieties, ka nav bloķēti uznirstošie logi)" github: title: "ar GitHub" - message: "Autorizēšanās ar GitHub (pārliecinieties, ka nav bloķēti uznirstošie logi)" invites: accept_title: "Ielūgums" welcome_to: "Laipni lūdzam %{site_name}!" @@ -1033,7 +1031,6 @@ lv: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -2286,6 +2283,7 @@ lv: user: "Lietotājs" title: "API" key: "API atslēga" + created: Radīts generate: "Radīt" regenerate: "Atkārtoti radīt" revoke: "Atsaukt" diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml index 61368db1ad..0ad211f6fb 100644 --- a/config/locales/client.nb_NO.yml +++ b/config/locales/client.nb_NO.yml @@ -751,11 +751,15 @@ nb_NO: second_factor: title: "Totrinnsverifisering" confirm_password_description: "Bekreft passordet ditt for å fortsette" + name: "Navn" label: "Kode" disable_description: "Vennligst oppgi autentiseringskoden fra appen" show_key_description: "Skriv inn manuelt" oauth_enabled_warning: "Vær oppmerksom på at sosiale login-metoder deaktiveres når tofaktor autentisering er aktivert på kontoen din." edit: "Rediger" + security_key: + register: "Registrer" + delete: 'Slett' change_about: title: "Rediger om meg" error: "Det oppstod en feil ved endring av denne verdien." @@ -1110,10 +1114,8 @@ nb_NO: password: "Passord" second_factor_title: "Totrinnsverifisering" second_factor_description: "Skriv inn verifiseringskoden fra din app:" - second_factor_backup: "Logg inn med en reservekode" second_factor_backup_title: "Tofaktor sikkerhetskode" second_factor_backup_description: "Vennligst skriv en av reservekodene dine:" - second_factor: "Logg inn med Authenticator-appen" email_placeholder: "e-postadresse eller brukernavn" caps_lock_warning: "Caps Lock er på" error: "Ukjent feil" @@ -1144,23 +1146,18 @@ nb_NO: google_oauth2: name: "Google" title: "med Google" - message: "Autentiserer med Google (sørg for at du tillater pop-up vindu)" twitter: name: "Twitter" title: "med Twitter" - message: "Autentiserer med Twitter (sørg for at du tillater oppsprettsvindu)" instagram: name: "Instagram" title: "med Instagram" - message: "Logger inn med Instagram. Sørg for at du ikke har programvare som blokkerer forgrunnsvinduer." facebook: name: "Facebook" title: "med Facebook" - message: "Autentiserer med Facebook (sørg for at du tillater oppsprettsvindu)" github: name: "GitHub" title: "med GitHub" - message: "Autentiserer med GitHub (sørg for at du tillater oppsprettsvindu)" invites: accept_title: "Invitasjon" welcome_to: "Velkommen til %{site_name}!" @@ -1178,7 +1175,6 @@ nb_NO: apple_international: "Apple/Internasjonalt" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Klassisk Google" facebook_messenger: "Facebook Meldingstjeneste" @@ -2638,6 +2634,7 @@ nb_NO: user: "Bruker" title: "API" key: "Nøkkel" + created: Opprettet generate: "Generer API-nøkkel" regenerate: "Regenerer API-nøkkel" revoke: "Trekk tilbake" diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml index 685e5cd8f8..000d7ffbaf 100644 --- a/config/locales/client.nl.yml +++ b/config/locales/client.nl.yml @@ -880,7 +880,7 @@ nl: copied_to_clipboard: "Gekopieerd naar klembord" copy_to_clipboard_error: "Fout bij kopiëren van gegevens naar klembord" remaining_codes: "U hebt {{count}} back-upcodes over." - use: "Een back-upcode gebruiken" + use: "Een back-upcode gebruiken" enable_prerequisites: "U moet een primaire tweede factor inschakelen voordat u back-upcodes genereert." codes: title: "Back-upcodes gegenereerd" @@ -888,7 +888,9 @@ nl: second_factor: title: "Twee-factor-authenticatie" enable: "Twee-factor-authenticatie beheren" + forgot_password: "Wachtwoord vergeten?" confirm_password_description: "Bevestig uw wachtwoord om door te gaan" + name: "Naam" label: "Code" rate_limit: "Wacht even voordat u een andere authenticatiecode probeert." enable_description: | @@ -900,7 +902,7 @@ nl: extended_description: | Twee-factor-authenticatie voegt extra beveiliging toe aan uw account door naast uw wachtwoord een eenmalige code te vereisen. Tokens kunnen op Android- en iOS-apparaten worden gegenereerd. oauth_enabled_warning: "Hou er rekening mee dat sociale aanmeldingen worden uitgeschakeld als u twee-factor-authenticatie op uw account inschakelt." - use: "Authenticator-app gebruiken" + use: "Authenticator-app gebruiken" enforced_notice: "U dient twee-factor-authenticatie in te schakelen voordat u deze website bezoekt." disable: "uitschakelen" disable_title: "Tweede factor uitschakelen" @@ -908,10 +910,21 @@ nl: edit: "Bewerken" edit_title: "Tweede factor bewerken" edit_description: "Naam van tweede factor" + enable_security_key_description: "Houd uw fysieke beveiligingssleutel gereed en klik op de onderstaande knop Registreren." totp: title: "Op tokens gebaseerde authenticators" add: "Nieuwe authenticator" default_name: "Mijn authenticator" + security_key: + register: "Registreren" + title: 'Beveiligingssleutels' + add: "Beveiligingssleutel registreren" + default_name: "Hoofdbeveiligingssleutel" + not_allowed_error: "Het registratieproces van de beveiligingssleutel had een time-out of is geannuleerd." + already_added_error: "U hebt deze beveiligingssleutel al geregistreerd. U hoeft deze niet opnieuw te registreren." + edit: 'Beveiligingssleutel bewerken' + edit_description: 'Naam van beveiligingssleutel' + delete: 'Verwijderen' change_about: title: "Over mij wijzigen" error: "Er is een fout opgetreden bij het wijzigen van deze waarde." @@ -1314,10 +1327,16 @@ nl: password: "Wachtwoord" second_factor_title: "Twee-factor-authenticatie" second_factor_description: "Voer de authenticatiecode van uw app in:" - second_factor_backup: "Aanmelden met een back-upcode" + second_factor_backup: "Aanmelden met een back-upcode" second_factor_backup_title: "Twee-factor-back-up" second_factor_backup_description: "Voer een van uw back-upcodes in:" - second_factor: "Aanmelden met authenticatie-app" + second_factor: "Aanmelden met authenticator-app" + security_key_description: "Houd uw fysieke beveiligingssleutel gereed en klik op de onderstaande knop Authenticeren met beveiligingssleutel." + security_key_alternative: "Uw beveiligingssleutel niet gevonden of een andere methode gebruiken?" + security_key_authenticate: "Authenticeren met beveiligingssleutel" + security_key_not_allowed_error: "Het authenticatieproces van de beveiligingssleutel had een time-out of is geannuleerd." + security_key_no_matching_credential_error: "Geen referenties gevonden in de opgegeven beveiligingssleutel." + security_key_support_missing_error: "Uw huidige apparaat of browser ondersteunt geen gebruik van beveiligingssleutels. Gebruik een andere methode." email_placeholder: "e-mailadres of gebruikersnaam" caps_lock_warning: "Caps Lock staat aan" error: "Onbekende fout" @@ -1350,27 +1369,24 @@ nl: google_oauth2: name: "Google" title: "met Google" - message: "Authenticeren met Google (zorg ervoor dat pop-upblokkeringen zijn uitgeschakeld)" twitter: name: "Twitter" title: "met Twitter" - message: "Authenticeren met Twitter (zorg ervoor dat pop-upblokkeringen zijn uitgeschakeld)" instagram: name: "Instagram" title: "met Instagram" - message: "Authenticeren met Instagram (zorg ervoor dat pop-upblokkeringen zijn uitgeschakeld)" facebook: name: "Facebook" title: "met Facebook" - message: "Authenticeren met Facebook (zorg ervoor dat pop-upblokkeringen zijn uitgeschakeld)" github: name: "GitHub" title: "met GitHub" - message: "Authenticeren met GitHub (zorg ervoor dat pop-upblokkeringen zijn uitgeschakeld)" discord: name: "Discord" title: "met Discord" - message: "Authenticeren met Discord" + second_factor_toggle: + totp: "Een authenticator-app gebruiken" + backup_code: "Een back-upcode gebruiken" invites: accept_title: "Uitnodiging" welcome_to: "Welkom bij %{site_name}!" @@ -1388,7 +1404,7 @@ nl: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" + emoji_one: "JoyPixels (voorheen EmojiOne)" win10: "Win10" google_classic: "Google Klassiek" facebook_messenger: "Facebook Messenger" @@ -3027,6 +3043,9 @@ nl: user: "Gebruiker" title: "API" key: "API-sleutel" + created: Gemaakt + last_used: Laatst gebruikt + never_used: (nooit) generate: "Genereren" regenerate: "Opnieuw genereren" revoke: "Intrekken" @@ -3349,7 +3368,7 @@ nl: other: "Thema loopt {{count}} commits achter!" compare_commits: "(Nieuwe commits bekijken)" repo_unreachable: "Kon geen contact krijgen met de Git-repository van dit thema. Foutbericht:" - imported_from_archive: "Dit thema is vanuit een .tar.gz-bestand geïmporteerd" + imported_from_archive: "Dit thema is vanuit een .zip-bestand geïmporteerd" scss: text: "CSS" title: "Voer aangepaste CSS in; we accepteren alle geldige CSS- en SCSS-stijlen" diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index 28ce2a6687..3571622478 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -882,13 +882,13 @@ pl_PL: enable_long: "Włącz kody zapasowe" copied_to_clipboard: "Skopiowane do schowka" copy_to_clipboard_error: "Wystąpił błąd w trakcie kopiowania do schowka" - use: "Użyj kodu zapasowego" codes: title: "Wygenerowano kody zapasowe" second_factor: title: "Dwuskładnikowe uwierzytelnianie" enable: "Zarządzaj autentykacją dwuetapową" confirm_password_description: "Potwierdź swoje hasło, aby kontynuować" + name: "Imię" label: "Kod" rate_limit: "Poczekaj, zanim spróbujesz użyć innego kodu uwierzytelniającego." disable_description: "Podaj kod uwierzytelniający ze swojej aplikacji" @@ -901,6 +901,10 @@ pl_PL: edit: "Edytuj" totp: title: "Uwierzytelnianie oparte na tokenach" + security_key: + register: "Zarejestruj" + edit: 'Edytuj Klucz Bezpieczeństwa' + delete: 'Usuń' change_about: title: "Zmień O mnie" error: "Wystąpił błąd podczas zmiany tej wartości." @@ -1295,7 +1299,6 @@ pl_PL: second_factor_title: "Dwuskładnikowe uwierzytelnianie" second_factor_description: "Podaj kod uwierzytelniający ze swojej aplikacji:" second_factor_backup_description: "Proszę wprowadź jeden ze swoich zapasowych kodów:" - second_factor: "1" email_placeholder: "adres email lub nazwa użytkownika" caps_lock_warning: "Caps Lock jest włączony" error: "Nieznany błąd" @@ -1325,23 +1328,18 @@ pl_PL: google_oauth2: name: "Google" title: "przez Google" - message: "Uwierzytelniam przy pomocy Google (upewnij się wyskakujące okienka nie są blokowane)" twitter: name: "Twitter" title: "przez Twitter" - message: "Uwierzytelnianie przy pomocy konta na Twitterze (upewnij się, że blokada wyskakujących okienek nie jest włączona)" instagram: name: "Instagram" title: "z Instagram" - message: "Uwierzytelnianie przy pomocy konta na Instagramie (upewnij się, że blokada wyskakujących okienek nie jest włączona)" facebook: name: "Facebook" title: "przez Facebook" - message: "Uwierzytelnianie przy pomocy konta Facebook (upewnij się, że blokada wyskakujących okienek nie jest włączona)" github: name: "GitHub" title: "przez GitHub" - message: "Uwierzytelnianie przez GitHub (upewnij się, że blokada wyskakujących okienek nie jest włączona)" discord: name: "Discord" invites: @@ -1361,7 +1359,6 @@ pl_PL: apple_international: "Apple/Międzynarodowy" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -2722,6 +2719,7 @@ pl_PL: activity_metrics: Metryka aktywności general_tab: "Ogólne" security_tab: "Bezpieczeństwo" + reports_tab: "Zgłoszenia" report_filter_any: "każdy" too_many_requests: "Wykonałeś tę akcję zbyt wiele razy. Poczekaj, zanim spróbujesz ponownie." not_found_error: "Przepraszamy, ten raport nie istnieje." @@ -2804,6 +2802,7 @@ pl_PL: user: "Użytkownik" title: "API" key: "Klucz API" + created: Utworzono generate: "Generuj" regenerate: "Odnów" revoke: "Unieważnij" @@ -2997,34 +2996,60 @@ pl_PL: long_title: "Zmodyfikuj kolory, kod CSS i kod HTML zawartości Twojej strony" edit: "Edytuj" edit_confirm: "To jest zdalny styl. Jeśli edytujesz CSS/HTML, twoje zmiany zostaną usunięte po ponownej aktualizacji motywu." + update_confirm_yes: "Tak, kontynuuj aktualizację" common: "Częste" desktop: "Komputer" mobile: "Mobilnie" settings: "Ustawienia" + translations: "Tłumaczenia" preview: "Podgląd" + show_advanced: "Pokaż pola zaawansowane" + hide_advanced: "Ukryj pola zaawansowane" + hide_unused_fields: "Ukryj nieużywane pola" is_default: "Motyw ustawiony jako domyślny" user_selectable: "Motyw może być ustawiany przez użytkowników" + color_scheme: "Paleta Kolorów" color_scheme_select: "Wybierz kolory, jakie mają być użyte w motywie" custom_sections: "Spersonalizowane sekcje:" theme_components: "Komponenty motywu" + inactive_themes: "Nieaktywne motywy:" + disabled_component_tooltip: "Ten komponent został wyłączony" + default_theme_tooltip: "Ten motyw jest domyślnym motywem witryny" + updates_available_tooltip: "Dostępne są aktualizacje dla tego motywu" collapse: Zwiń uploads: "Pliki" no_uploads: "Możesz załadować zasoby do swojego motywu np. fonty lub obrazy" add_upload: "Dodaj plik" upload_file_tip: "Wybierz plik do wysłania (png, woff2, itp.)" variable_name: "Nazwa zmiennej SCSS:" + variable_name_error: + must_be_unique: "Niepoprawna nazwa zmiennej. Musi być unikalna." upload: "Plik" + unsaved_changes_alert: "Nie zapisałeś jeszcze zmian, czy chcesz je odrzucić i przejść dalej?" + discard: "Odrzuć" css_html: "Własny CSS/HTML" edit_css_html: "Edytuj CSS/HTML" edit_css_html_help: "Nie modyfikowałeś CSS ani HTML" delete_upload_confirm: "Czy usunąć ten plik? (CSS motywu może przestać działać!)" import_web_tip: "Repozytorium zawierające motyw" + remote_branch: "Nazwa oddziału (opcjonalnie)" installed: "Zainstalowana" install_popular: "Popularne" + install_upload: "Z twojego urządzenia" + install_git_repo: "Z repozytorium git" + install_create: "Stwórz nowy" about_theme: "O stronie" license: "Licencja" + version: "Wersja:" + authors: "Autor:" + source_url: "Źródło" enable: "Włącz" disable: "Wyłącz" + disabled: "Ten komponent został wyłączony." + disabled_by: "Ten komponent został wyłączony przez" + required_version: + error: "Ten motyw został automatycznie wyłączony, ponieważ nie jest kompatybilny z tą wersją Discourse." + component_of: "Składnik:" update_to_latest: "Aktualizuj do najnowszego" check_for_updates: "Sprawdź dostępność aktualizacji" updating: "Trwa proces aktualizacji..." @@ -3039,6 +3064,7 @@ pl_PL: few: "Motyw jest {{count}} aktualizacji w tyle!" many: "Motyw jest {{count}} aktualizacji w tyle!" other: "Motyw jest {{count}} aktualizacji w tyle!" + repo_unreachable: "Nie można skontaktować się z repozytorium Git tego motywu. Komunikat o błędzie:" scss: text: "CSS" title: "Wstaw własny CSS, przyjmujemy wszystkie prawidłowe style CSS i SCSS" @@ -3063,8 +3089,16 @@ pl_PL: yaml: text: "YAML" colors: + select_base: + title: "Wybierz podstawową paletę kolorów" + description: "Paleta podstawowa:" title: "Kolory" + edit: "Edytuj palety kolorów" + long_title: "Palety kolorów" + about: "Zmodyfikuj kolory używane w swoich motywach. Utwórz nową paletę kolorów, aby rozpocząć." + new_name: "Nowa paleta kolorów" copy_name_prefix: "Kopia" + delete_confirm: "Usunąć tę paletę kolorów?" undo: "cofnij" undo_title: "Cofnij zmiany tego koloru od ostatniego zapisu" revert: "przywróć" @@ -3100,11 +3134,15 @@ pl_PL: description: "Kolor przycisku lajkuj" email_style: css: "CSS" + reset: "Przywróć ustawienia domyślne" email: title: "Emaile" settings: "Ustawienia" templates: "Szablony" preview_digest: "Pokaż zestawienie aktywności" + advanced_test: + email: " Oryginalna wiadomość" + run: "Przeprowadź Test" sending_test: "Wysyłanie testowego emaila…" error: "BŁAD - %{server_error}" test_error: "Wystąpił problem podczas wysyłania testowego maila. Sprawdź ustawienia poczty, sprawdź czy Twój serwer nie blokuje połączeń pocztowych i spróbuj ponownie." @@ -3227,6 +3265,7 @@ pl_PL: delete_category: "Usuń kategorię" create_category: "Dodaj nową kategorię" silence_user: "wycisz użytkownika" + unsilence_user: "cofnij wyciszenie użytkownika" grant_admin: "nadaj prawa admina" revoke_admin: "odbierz prawa admina" grant_moderation: "Przyznaj status moderatora" @@ -3244,6 +3283,7 @@ pl_PL: backup_destroy: "Zniszcz kopię zapasową " reviewed_post: "przejrzane posty" custom_staff: "spersonalizowana akcja wtyczki" + post_rejected: "wpis odrzucony" change_name: "zmień nazwe" screened_emails: title: "Ekranowane emaile" @@ -3291,6 +3331,7 @@ pl_PL: search: "szukaj" clear_filter: "Wyczyść" show_words: "pokaż słowa" + one_word_per_line: "Jedno słowo w wierszu" download: Pobierz clear_all: Wyczyść wszystko word_count: @@ -3314,9 +3355,12 @@ pl_PL: placeholder_regexp: "wyrażenie regularne" add: "Dodaj" success: "Sukces" + exists: "Już istnieje" + upload: "Dodaj z pliku" upload_successful: "Przesyłanie zakończone sukcesem. Słowa zostały dodane." test: button_label: "Sprawdź" + description: "Wpisz tekst poniżej, aby sprawdzić dopasowania z obserwowanymi słowami" no_matches: "Nie znaleziono dopasowań" impersonate: title: "Zaloguj się na to konto" @@ -3329,7 +3373,9 @@ pl_PL: last_emailed: "Ostatnio wysłano email" not_found: "Przepraszamu, taka nazwa użytkowanika nie istnieje w naszym systemie." id_not_found: "Przepraszamy, ten identyfikator użytkownika nie istnieje w naszym systemie." + active: "Aktywowane" show_emails: "Pokaż emaile" + hide_emails: "Ukryj e-maile" nav: new: "Nowi" active: "Aktywni" @@ -3379,9 +3425,19 @@ pl_PL: suspended_until: "(do %{until})" cant_suspend: "Nie można zawiesić tego użytkownika." delete_all_posts: "Usuń wszystkie wpisy" + delete_posts_progress: "Usuwanie wpisów..." + delete_posts_failed: "Podczas usuwania wpisów wystąpił problem." + penalty_post_actions: "Co chcesz zrobić z powiązanym wpisem?" penalty_post_delete: "Usuń ten wpis" + penalty_post_delete_replies: "Usuń wpis + wszelkie odpowiedzi" + penalty_post_edit: "Edytuj wpis" + penalty_post_none: "Nic nie rób" + penalty_count: "Liczba Przewinień" + clear_penalty_history: + title: "Wyczyść Historię Przewinień" delete_all_posts_confirm_MF: "Zamierzasz usunąć {POSTS, plural, one {1 post} few {# posty} many {# postów} other {# postów}} i {TOPICS, plural, one {1 temat} few {# tematy} many {# tematów} other {# tematów}}. Czy jesteś pewien?" silence: "Wycisz" + unsilence: "Cofnij wyciszenie" silenced: "Wyciszony?" moderator: "Moderator?" admin: "Admin?" @@ -3455,7 +3511,9 @@ pl_PL: activate_failed: "Wystąpił problem przy aktywacji konta użytkownika." deactivate_account: "Deaktywuj konto" deactivate_failed: "Wystąpił problem przy deaktywacji konta użytkownika." + unsilence_failed: "Wystąpił problem podczas odblokowywania wyciszenia użytkownika." silence_failed: "Wystąpił problem podczas wyciszania użytkownika." + silence_confirm: "Czy na pewno chcesz wyciszyć tego użytkownika? Nie będzie mógł tworzyć nowych tematów ani postów." silence_accept: "Tak, ucisz tego użytkownika" bounce_score: "Wskaźnik odbić" reset_bounce_score: @@ -3558,6 +3616,7 @@ pl_PL: go_back: "Wróć do wyszukiwania" recommended: "Zalecamy zmianę poniższego tekstu, aby lepiej odpowiadał Twoim potrzebom:" show_overriden: "Pokaż tylko nadpisane" + more_than_50_results: "Istnieje ponad 50 wyników. Zawęź wyszukiwanie." settings: show_overriden: "Pokaż tylko nadpisane" reset: "przywróć domyślne" @@ -3565,14 +3624,19 @@ pl_PL: site_settings: title: "Ustawienia" no_results: "Brak wyników wyszukiwania" + more_than_30_results: "Istnieje ponad 30 wyników. Zawęź wyszukiwanie lub wybierz kategorię." clear_filter: "Wyczyść" add_url: "dodaj URL" add_host: "dodaj host" add_group: "dodaj grupę" uploaded_image_list: label: "Edytuj listę" + empty: "Brak zdjęć. Prześlij jedno." upload: label: "Prześlij" + title: "Prześlij obrazek" + selectable_avatars: + title: "Lista awatarów, z których użytkownicy mogą wybierać" categories: all_results: "Wszystkie" required: "Wymagane" @@ -3638,6 +3702,7 @@ pl_PL: enabled: Włącz odznakę icon: Ikona image: Grafika + image_help: "Wprowadź adres URL obrazu (zastępuje pole ikony, jeśli oba są ustawione)" query: "Zapytanie odznaki (SQL) " target_posts: Wpisy powiązane z odznaką auto_revoke: Codziennie uruchamiaj zapytanie odbierające odznakę @@ -3672,6 +3737,7 @@ pl_PL: with_post_time: "%{username} za wpis w %{link} o %{time}" with_time: "%{username} o %{time}" badge_intro: + title: "Wybierz istniejącą odznakę lub utwórz nową, aby rozpocząć" what_are_badges_title: "Czym są odznaki?" emoji: title: "Emoji" diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml index 1bcc2ba7f3..d976f7b595 100644 --- a/config/locales/client.pt.yml +++ b/config/locales/client.pt.yml @@ -746,13 +746,13 @@ pt: copied_to_clipboard: "Copiado para a Área de Transferência" copy_to_clipboard_error: "Erro a copiar dados para a Área de Transferência" remaining_codes: "Restam {{count}} códigos de reserva." - use: "Usar um código de reserva" codes: title: "Códigos de Reserva Gerados" description: "Cada um dos códigos de reserva só pode ser usado uma vez. Guarde-os num sítio seguro mas acessível." second_factor: title: "Autenticação em Dois Passos" confirm_password_description: "Por favor confirme a sua palavra-passe para continuar" + name: "Nome" label: "Código" rate_limit: "Por favor espere antes de voltar a tentar um código de autenticação." disable_description: "Por favor introduza o código de autenticação a partir da sua aplicação" @@ -760,9 +760,11 @@ pt: extended_description: | A autenticação em dois passos adiciona segurança à sua conta ao pedir um código de utilização única além da sua palavra-passe. Os códigos podem ser gerados em dispositivos Android e iOS. oauth_enabled_warning: "Note que os logins com contas sociais vão ser desativados depois da autenticação em dois passos ser ativa na sua conta." - use: "Usar aplicação Authenticator" enforced_notice: "É necessário ativar a autenticação em dois passos para aceder a este site." edit: "Editar" + security_key: + register: "Registar" + delete: 'Eliminar' change_about: title: "Modificar Sobre Mim" error: "Ocorreu um erro ao modificar este valor." @@ -1144,10 +1146,8 @@ pt: password: "Palavra-passe" second_factor_title: "Autenticação em Dois Passos" second_factor_description: "Por favor introduza um código de autenticação a partir da sua aplicação:" - second_factor_backup: "Entrar com um código de reserva" second_factor_backup_title: "Cópia de Segurança do Segundo Passo de Autenticação" second_factor_backup_description: "Por favor introduza um dos seus códigos de reserva:" - second_factor: "Entrar com a aplicação Authenticator" email_placeholder: "e-mail ou nome de utilizador" caps_lock_warning: "Caps Lock está ligada" error: "Erro desconhecido" @@ -1179,23 +1179,18 @@ pt: google_oauth2: name: "Google" title: "com Google" - message: "A autenticar com Google (certifique-se de que os bloqueadores de janela estão desativados)" twitter: name: "Twitter" title: "com Twitter" - message: "A autenticar com Twitter (certifique-se de que os bloqueadores de janela estão desativados)" instagram: name: "Instagram" title: "com Instagram" - message: "A autenticar com Instagram (certifique-se de que os bloqueadores de janela estão desativados)" facebook: name: "Facebook" title: "com Facebook" - message: "A autenticar com o Facebook (certifique-se de que os bloqueadores de janela estão desativados)" github: name: "GitHub" title: "com GitHub" - message: "A autenticar com GitHub (certifique-se de que os bloqueadores de janela estão desativados)" invites: accept_title: "Convite" welcome_to: "Bem-vindo a %{site_name}!" @@ -1213,7 +1208,6 @@ pt: apple_international: "Apple/Internacional" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -2591,6 +2585,7 @@ pt: user: "Utilizador" title: "API" key: "Chave API" + created: Criado generate: "Gerar" regenerate: "Regenerar" revoke: "Revogar" diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index ada8dbd85f..8e3e5ba50b 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -853,7 +853,6 @@ pt_BR: copied_to_clipboard: "Copiado para a Área de Transferência" copy_to_clipboard_error: "Erro ao copiar dados para a Área de Transferência" remaining_codes: "Você tem {{count}} códigos de backup restantes." - use: "Usar um código de backup" enable_prerequisites: "Você deve habilitar um segundo fator primário antes de gerar códigos de backup." codes: title: "Códigos de Backup Gerados" @@ -862,6 +861,7 @@ pt_BR: title: "Autenticação de Dois Fatores" enable: "Gerenciar Autenticação de Dois Fatores" confirm_password_description: "Por favor, confirme sua senha para continuar" + name: "Nome" label: "Código" rate_limit: "Por favor, aguarde antes de tentar outro código de autenticação." enable_description: | @@ -873,7 +873,6 @@ pt_BR: extended_description: | A autenticação de dois fatores adiciona segurança extra à sua conta, exigindo um token único além da sua senha. Tokens podem ser gerados em dispositivos Android e iOS. oauth_enabled_warning: "Por favor, observe que os logins sociais serão desabilitados quando a autenticação de dois fatores for habilitada na sua conta." - use: "Usar um aplicativo autenticador" enforced_notice: "Você precisa ativar a autenticação de dois fatores antes de acessar este site." disable: "Desabilitar" disable_title: "Desabilitar Segundo Fator" @@ -885,6 +884,9 @@ pt_BR: title: "Autenticadores Baseados em Token" add: "Novo Autenticador" default_name: "Meu Autenticador" + security_key: + register: "Registro" + delete: 'Excluir' change_about: title: "Modificar Sobre Mim" error: "Houve um erro ao alterar este valor." @@ -1284,10 +1286,8 @@ pt_BR: password: "Senha" second_factor_title: "Autenticação de Dois Fatores" second_factor_description: "Por favor, digite o código de autenticação do seu aplicativo:" - second_factor_backup: "Entrar usando um código de backup" second_factor_backup_title: "Backup de Dois Fatores" second_factor_backup_description: "Por favor, insira um dos seus códigos de backup:" - second_factor: "Entrar usando um aplicativo autenticador" email_placeholder: "e-mail ou nome de usuário" caps_lock_warning: "Caps Lock está ativado" error: "Erro desconhecido" @@ -1320,25 +1320,23 @@ pt_BR: google_oauth2: name: "Google" title: "com Google" - message: "Autenticando com Google (certifique-se de que os bloqueadores de pop-up estejam desabilitados)" twitter: name: "Twitter" title: "com Twitter" - message: "Autenticando com Twitter (certifique-se de que os bloqueadores de pop-up estejam desabilitados)" instagram: name: "Instagram" title: "com Instagram" - message: "Autenticando com Instagram (certifique-se de que os bloqueadores de pop-up estejam desabilitados)" facebook: name: "Facebook" title: "com Facebook" - message: "Autenticando com Facebook (certifique-se de que os bloqueadores de pop-up estejam desabilitados)" github: name: "GitHub" title: "com GitHub" - message: "Autenticando com GitHub (certifique-se de que os bloqueadores de pop-up estejam desabilitados)" discord: name: "Discord" + second_factor_toggle: + totp: "Use um aplicativo autenticador" + backup_code: "Use um código de backup" invites: accept_title: "Convite" welcome_to: "Bem vindo à %{site_name}!" @@ -1356,7 +1354,6 @@ pt_BR: apple_international: "Apple/Internacional" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -2985,6 +2982,7 @@ pt_BR: user: "Usuário" title: "API" key: "Chave API" + created: Criado generate: "Gerar" regenerate: "Regenerar" revoke: "Revogar" @@ -3304,7 +3302,6 @@ pt_BR: other: "o tema é {{count}} confirmado por trás" compare_commits: "(Ver novos commits)" repo_unreachable: "Não foi possível contatar o repositório Git deste tema. Mensagem de erro:" - imported_from_archive: "Este tema foi importado de um arquivo .tar.gz" scss: text: "CSS" title: "Digite CSS personalizado, aceitamos todos os estilos CSS e SCSS válidos" diff --git a/config/locales/client.ro.yml b/config/locales/client.ro.yml index ed79cc1d94..1df623cbe3 100644 --- a/config/locales/client.ro.yml +++ b/config/locales/client.ro.yml @@ -712,7 +712,11 @@ ro: copied_to_clipboard: "Copiat în clipboard" copy_to_clipboard_error: "Eroare la copierea datelor în clipboard" second_factor: + name: "Nume" edit: "Editează" + security_key: + register: "Înregistrare" + delete: 'Șterge' change_about: title: "Schimbare date personale" error: "A apărut o eroare la schimbarea acestei valori." @@ -1088,20 +1092,15 @@ ro: google_oauth2: name: "Google" title: "Google" - message: "Autentificare cu Google (Verifică browserul să nu blocheze ferestrele pop-up)" twitter: name: "Twitter" title: "Twitter" - message: "Autentificare cu Twitter (Verifică browserul să nu blocheze ferestrele pop-up)" instagram: title: "Instagram" - message: "Autentificare cu Instagram (Verifică browserul să nu blocheze ferestrele pop-up)" facebook: title: "Facebook" - message: "Autentificare cu Facebook (Verifică browserul să nu blocheze ferestrele pop-up)" github: title: "GitHub" - message: "Autentificare cu GitHub (Verifică browserul să nu blocheze ferestrele pop-up)" invites: accept_title: "Initație" welcome_to: "Bine ai venit la %{site_name}!" @@ -1119,7 +1118,6 @@ ro: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google clasic" facebook_messenger: "Facebook Messenger" @@ -2479,6 +2477,7 @@ ro: user: "Utilizator" title: "API" key: "Cheie API" + created: Creat generate: "Generare" regenerate: "Regenerare" revoke: "Revocă" diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index 9eb2defe37..40a7b6fee9 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -960,7 +960,7 @@ ru: copied_to_clipboard: "Скопировано в буфер" copy_to_clipboard_error: "Ошибка при копировании данных в буфер омена" remaining_codes: "У вас осталось {{count}}резервных кодов" - use: "Использовать резервный код" + use: "Используйте резервный код" enable_prerequisites: "Вы должны включить первичный второй фактор перед генерацией резервных кодов." codes: title: "Резервные коды созданы" @@ -968,7 +968,9 @@ ru: second_factor: title: "Двухфакторная аутентификация" enable: "Управление Двухфакторной Аутентификацией" + forgot_password: "Забыли пароль?" confirm_password_description: "Подтвердите ваш пароль чтобы продолжить" + name: "Имя" label: "Код" rate_limit: "Пожалуйста, подождите, прежде чем попробовать другой код аутентификации." enable_description: | @@ -980,7 +982,7 @@ ru: extended_description: | Двухфакторная аутентификация повышает безопасность вашей учетной записи, требуя одноразовый токен в дополнение к вашему паролю. Токены могут быть сгенерированы на устройствах Android и iOS. oauth_enabled_warning: "Обратите внимание, что социальные логины будут отключены после включения двухфакторной аутентификации в вашей учетной записи." - use: "Используйте приложение для проверки подлинности" + use: "Используйте приложение для проверки подлинности " enforced_notice: "Вы должны включить двухфакторную аутентификацию перед доступом к этому сайту." disable: "отключить" disable_title: "Отключить Второй Фактор" @@ -988,10 +990,21 @@ ru: edit: "Редактировать" edit_title: "Изменение Второго Фактора" edit_description: "Имя Второго Фактора" + enable_security_key_description: "Когда вы подготовите свой физический ключ безопасности, нажмите кнопку Регистрация ниже." totp: title: "Token-Based Аутентификация" add: "Новый Аутентификатор" default_name: "Мой Аутентификатор" + security_key: + register: "Зарегистрироваться" + title: 'Ключи безопасности' + add: "Зарегистрировать ключ безопасности" + default_name: "Главный Ключ Безопасности" + not_allowed_error: "Время регистрации ключа безопасности истекло или было отменено." + already_added_error: "Вы уже зарегистрировали этот ключ безопасности. Вам не нужно регистрировать его снова." + edit: 'Изменить Ключ Безопасности' + edit_description: 'Имя Ключа Безопасности' + delete: 'Удалить' change_about: title: "Изменить информацию обо мне" error: "При изменении значения произошла ошибка." @@ -1416,10 +1429,16 @@ ru: password: "Пароль" second_factor_title: "Двухфакторная аутентификация" second_factor_description: "Введите код аутентификации из вашего приложения:" - second_factor_backup: "Войти с помощью запасного кода" + second_factor_backup: "Войти с помощью запасного кода" second_factor_backup_title: "Запасной вход двухфакторной аутентификации" second_factor_backup_description: "Введите запасной код:" - second_factor: "Войти с помощью программы аутентификации" + second_factor: "Войти с помощью программы аутентификации" + security_key_description: "Когда вы подготовите свой физический ключ безопасности, нажмите кнопку Аутентификация с ключом безопасности ниже." + security_key_alternative: "Не удается найти ключ безопасности или хотите использовать другой метод?" + security_key_authenticate: "Аутентификация с Ключом Безопасности." + security_key_not_allowed_error: "Время проверки подлинности ключа безопасности истекло или было отменено." + security_key_no_matching_credential_error: "В указанном ключе безопасности не найдено подходящих учетных данных." + security_key_support_missing_error: "Ваше текущее устройство или браузер не поддерживает использование ключей безопасности. Пожалуйста, используйте другой метод." email_placeholder: "E-mail или псевдоним" caps_lock_warning: "Caps Lock включен" error: "Неизвестная ошибка" @@ -1452,27 +1471,24 @@ ru: google_oauth2: name: "Google" title: "Google" - message: "Вход с помощью учетной записи Google (убедитесь, что блокировщик всплывающих окон отключен)" twitter: name: "Twitter" title: "Twitter" - message: "Вход с помощью учетной записи Twitter (убедитесь, что блокировщик всплывающих окон отключен)" instagram: name: "Instagram" title: "Instagram" - message: "Вход с помощью учетной записи Instagram (убедитесь, что блокировщик всплывающих окон отключен)" facebook: name: "Facebook" title: "Facebook" - message: "Вход с помощью учетной записи Facebook (всплывающие окна должны быть разрешены)" github: name: "GitHub" title: "GitHub" - message: "Вход с помощью учетной записи GitHub (убедитесь, что блокировщик всплывающих окон отключен)" discord: name: "Discord" title: "с Discord" - message: "Аутентификация с помощью Discord" + second_factor_toggle: + totp: "Вместо этого используйте приложение для проверки подлинности" + backup_code: "Вместо этого используйте резервный код" invites: accept_title: "Приглашение" welcome_to: "Добро пожаловать на %{site_name}!" @@ -1490,7 +1506,7 @@ ru: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" + emoji_one: "JoyPixels (ранее EmojiOne)" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -3253,6 +3269,7 @@ ru: user: "Пользователь" title: "API" key: "Ключ API" + created: Создано generate: "Сгенерировать" regenerate: "Перегенерировать" revoke: "Отозвать" @@ -3581,7 +3598,7 @@ ru: other: "Тема находится на {{count}} коммитов сзади!" compare_commits: "(Смотрите новые коммиты)" repo_unreachable: "Не удалось связаться с Git-репозиторием этой темы. Сообщение об ошибке:" - imported_from_archive: "Эта тема была импортирована из .tar.gz файла" + imported_from_archive: "Эта тема была импортирована из .zip файла" scss: text: "CSS" title: "Введите CSS; допускаются все стили CSS и SCSS" diff --git a/config/locales/client.sk.yml b/config/locales/client.sk.yml index 09de5c718a..72417ef9eb 100644 --- a/config/locales/client.sk.yml +++ b/config/locales/client.sk.yml @@ -765,6 +765,7 @@ sk: second_factor: title: "Dvojfaktorová autentifikácia" confirm_password_description: "Pre pokračovanie potvrďte svoje heslo prosím" + name: "Meno" label: "Kód" rate_limit: "Prosím čakajte pred zadaním ďalšieho autentifikačného kódu" disable_description: "Prosím zadajte autentifikačný kód z vašej aplikácie" @@ -772,6 +773,8 @@ sk: extended_description: | Dvojfaktorové prihlásenie pridáva dodatočné zabezpečenie Vášho účtu prostredníctvom zadávania jednorázových hesiel. Heslá môžu byť generované na Android aiOS zariadeniach. edit: "Upraviť" + security_key: + delete: 'Odstrániť' change_about: title: "Upraviť O mne" error: "Nastala chyba pri zmene tejto hodnoty." @@ -1154,20 +1157,15 @@ sk: google_oauth2: name: "Google" title: "pomocou Google" - message: "Prihlásenie pomocou Google účtu (prosím uistite sa, že vyskakovacie okná sú povolené)" twitter: name: "Twitter" title: "pomocou Twitter účtu" - message: "Prihlásenie pomocou Twitter účtu (prosím uistite sa, že vyskakovacie okná sú povolené)" instagram: title: "so službou Instagram" - message: "Prihlásenie pomocou Instagram účtu (prosím uistite sa, že vyskakovacie okná sú povolené)" facebook: title: "pomocou stránky Facebook" - message: "Prihlásenie pomocou Facebook účtu (prosím uistite sa, že vyskakovacie okná sú povolené)" github: title: "pomocou GitHub" - message: "Prihlásenie pomocou GitHub účtu (prosím uistite sa, že vyskakovacie okná sú povolené)" discord: name: "Discord" invites: @@ -1184,7 +1182,6 @@ sk: apple_international: "Apple/Medzinárodné" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" category_page_style: categories_only: "Iba kategórie" @@ -2385,6 +2382,7 @@ sk: user: "Používateľ" title: "API" key: "API kľúč" + created: Vytvorené generate: "Generovať" regenerate: "Obnov" revoke: "Zrušiť" diff --git a/config/locales/client.sl.yml b/config/locales/client.sl.yml index 0e73a14bc3..07b9ed9e8e 100644 --- a/config/locales/client.sl.yml +++ b/config/locales/client.sl.yml @@ -129,6 +129,16 @@ sl: two: "pred %{count} dnevoma" few: "pred %{count} dnevi" other: "pred %{count} dnevi" + x_months: + one: "pred %{count} mesecom" + two: "pred %{count} mesecoma" + few: "pred %{count} meseci" + other: "pred %{count} meseci" + x_years: + one: "pred %{count} letom" + two: "pred %{count} letoma" + few: "pred %{count} leti" + other: "pred %{count} leti" later: x_days: one: "čez %{count} dan" @@ -267,6 +277,7 @@ sl: other: "{{count}} znakov" related_messages: title: "Povezana sporočila" + see_all: 'Poglej vsa sporočila od @%{username}...' suggested_topics: title: "Predlagane teme" pm_title: "Predlagana sporočila" @@ -276,6 +287,7 @@ sl: stats: "Statistika strani" our_admins: "Naši administratorji" our_moderators: "Naši moderatorji" + moderators: "Moderatorji" stat: all_time: "Ves čas" last_7_days: "Zadnjih 7" @@ -345,6 +357,8 @@ sl: banner: close: "Opusti ta oglasni trak." edit: "Uredi ta oglasni trak >>" + pwa: + install_banner: "Ali želite namestiti %{title} na to napravo?" choose_topic: none_found: "Ni tem." title: @@ -356,6 +370,8 @@ sl: search: "Poišči sporočilo po naslovu:" placeholder: "vnesite naslov sporočila tukaj" review: + order_by: "Uredi po" + in_reply_to: "v odgovor na" awaiting_approval: "Čaka odobritev" delete: "Izbriši" settings: @@ -606,6 +622,7 @@ sl: remove_owner: "Odstrani kot skrbnika" remove_owner_description: "Odstrani %{username} kot skrbnika te skupine" owner: "Skrbnik" + forbidden: "Nimate dovoljenja da vidite člane." topics: "Teme" posts: "Prispevki" mentions: "Omembe" @@ -618,6 +635,7 @@ sl: only_admins: "Le administratorji" mods_and_admins: "Le moderatorji in administratorji" members_mods_and_admins: "Le člani skupine, moderatorji in administratorji" + owners_mods_and_admins: "Samo za skrbnike skupin, moderatorje in administratorje" everyone: "Vsi" notifications: watching: @@ -770,6 +788,7 @@ sl: allow_private_messages: "Dovoli drugim uporabnikom da mi pošiljajo zasebna sporočila." external_links_in_new_tab: "Odpri vse zunanje povezave v novem zavihku" enable_quoting: "Omogoči odgovarjanje s citiranjem za poudarjen tekst" + enable_defer: "Omogoči preloži za označiti teme kot neprebrane" change: "spremeni" moderator: "{{user}} je moderator" admin: "{{user}} je administrator" @@ -809,6 +828,7 @@ sl: watched_first_post_tags_instructions: "Obveščeni boste ob prvem prispevku v novi temi s to oznako." muted_categories: "Utišano" muted_categories_instructions: "Nikoli ne boste obveščeni o novih temah v tej kategoriji in ne bodo se prikazovale med najnovejšimi." + muted_categories_instructions_dont_hide: "Ne boste obveščeni o ničemer o novimi temami v teh kategorijah" no_category_access: "Kot moderator imaš omejen dostop do kategorije, shranjevanje je onemogočeno" delete_account: "Izbriši moj račun" delete_account_confirm: "Ste prepričani, da želite trajno izbrisati svoj račun? Tega postopka ni mogoče razveljaviti!" @@ -876,13 +896,14 @@ sl: copied_to_clipboard: "Prenešeno na odlagališče" copy_to_clipboard_error: "Napaka pri prenosu na odlagališče" remaining_codes: "Imate {{count}} rezervnih potrditvenih kod." - use: "Uporabi rezervno kodo" codes: title: "Rezervne potrditvene kode ustvarjene" description: "Vsaka od rezervnih potrditvenih kod se lahko uporabi samo enkrat. Shranite jih na varno, ampak dostopno mesto." second_factor: title: "Preverjanje v dveh korakih" + forgot_password: "Ste pozabili geslo?" confirm_password_description: "Vnesite geslo za nadaljevanje" + name: "Polno ime" label: "Koda" rate_limit: "Počakajte preden uporabite novo potrditveno kodo." enable_description: | @@ -892,9 +913,10 @@ sl: extended_description: | Preverjanje v dveh korakih omogoči dodatno varnost vašega računa saj za prijavo zahteva dodatno enkratno potrditveno kodo poleg vašega gesla. Potrditvene kode se lahko ustvarijo na Android ali iOS napravah. oauth_enabled_warning: "Preverjanje v dveh korakih bo onemogočila prijavo z družabnimi omrežji." - use: "Uporabi Authenticator aplikacijo" enforced_notice: "Obvezno morate vklopiti preverjanje v dveh korakih za dostop to tega spletnega mesta." edit: "Uredi" + security_key: + delete: 'Izbriši' change_about: title: "Spremeni O meni" error: "Prišlo je do napake pri spreminjanju te vrednosti" @@ -932,7 +954,7 @@ sl: secondary: "Dodatni e-naslovi" no_secondary: "Ni dodatnih e-naslovov" sso_override_instructions: "E-naslov se lahko spremeni pri SSO ponudniku." - instructions: "Se nikoli ne prikaže javno." + instructions: "se nikoli ne prikaže javno" ok: "Poslali vam bomo e-sporočilo za potrditev." invalid: "Vnesite veljaven e-naslov." authenticated: "Vaša e-naslov je bil potrjen pri {{provider}}" @@ -948,6 +970,10 @@ sl: revoke: "Prekliči" cancel: "Prekliči" not_connected: "(ni povezano)" + confirm_modal_title: "Poveži %{provider} račun" + confirm_description: + account_specific: "Vaš %{provider} račun '%{account_description}' bo uporabljen za avtentikacijo." + generic: "Vaš %{provider} račun bo uporabljen za avtentikacijo." name: title: "Polno ime" instructions: "vaše polno ime (neobvezno)" @@ -1272,6 +1298,7 @@ sl: trust_level: "Nivo zaupanja" search_hint: "uporabniško ime, e-naslov ali IP naslov" create_account: + disclaimer: "Z registracijo sprejemate Pravilnik o zasebnosti in Pogoje uporabe." title: "Registracija uporabnika" failed: "Nekaj je šlo narobe, morda je ta e-naslov že registriran, poskusite povezavo za pozabljeno geslo." forgot_password: @@ -1298,16 +1325,16 @@ sl: complete_username_not_found: "Račun z uporabniškim imenom %{username} ne obstaja." complete_email_not_found: "Noben račun se ne ujema z %{email}" confirm_title: "Nadaljujte na %{site_name}" + logging_in_as: "Prijava kot %{email}" + confirm_button: Zaključi prijavo login: title: "Prijava" username: "Uporabnik" password: "Geslo" second_factor_title: "Preverjanje v dveh korakih" second_factor_description: "Vnesite potrditveno kodo iz vaše aplikacije: " - second_factor_backup: "Prijava z rezervno potrditveno kodo" second_factor_backup_title: "Rezervno preverjanje v dveh korakih" second_factor_backup_description: "Vnesite eno od rezervnih potrditvenih kod:" - second_factor: "Prijava preko Authenticator " email_placeholder: "e-naslov ali uporabniško ime" caps_lock_warning: "Caps Lock je vključen" error: "Neznana napaka" @@ -1340,23 +1367,21 @@ sl: google_oauth2: name: "Google" title: "Google" - message: "Preverjanje z Googlom (pojavna okna morajo biti omogočena v brskalniku)" twitter: name: "Twitter" title: "Twitter" - message: "Preverjanje s Twittrom (pojavna okna morajo biti omogočena v brskalniku)" instagram: name: "Instagram" title: "Instagram" - message: "Preverjanje z Instagramom (pojavna okna morajo biti omogočena v brskalniku)" facebook: name: "Facebook" title: "Facebook" - message: "Preverjanje s Facebookom (pojavna okna morajo biti omogočena v brskalniku)" github: name: "GitHub" title: "GitHub" - message: "Preverjanje z GitHubom (pojavna okna morajo biti omogočena v brskalniku)" + second_factor_toggle: + totp: "Namesto tega uporabite authenticator aplikacijo" + backup_code: "Namesto tega uporabite rezervno potrditveno kodo" invites: accept_title: "Povabilo" welcome_to: "Dobrodošli na %{site_name}!" @@ -1374,7 +1399,6 @@ sl: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -1412,7 +1436,9 @@ sl: other: "Izberite vsaj {{count}} stvari." date_time_picker: from: Od - to: Za + to: Do + errors: + to_before_from: "Datum Od mora biti starejši kot Do datum" emoji_picker: filter_placeholder: Išči emoji smileys_&_emotion: Smeški in emotikoni @@ -1464,14 +1490,17 @@ sl: category: "Omenil si uporabnika {{username}}, a ta ne bo obveščen saj nima dostopa do te kategorije. Dodati ga je potrebno v skupino ki lahko dostopa do kategorije." private: "Omenili ste uporabnika {{username}}, a ta ne bo obveščen, ker ne more videti tega zasebnega sporočila. Moraii ga boste povabiti v to zasebno sporočilo." duplicate_link: "Povezava do {{domain}} bila že objavljena v temi od @{{username}} v odgovoru {{ago}} – ali ste prepričani, da jo želite objaviti še enkrat?" + reference_topic_title: "RE: {{title}}" error: title_missing: "Naslov je obvezen" title_too_short: "Naslov mora vsebovati vsaj {{min}} znakov" title_too_long: "Naslov ne more imeti več kot {{max}} znakov" + post_missing: "Prispevek ne more biti prazen" post_length: "Prispevek mora vsebovati vsaj {{min}} znakov" try_like: "Ste že uporabili {{heart}} gumb za všečkanje?" category_missing: "Izbrati morate kategorijo" tags_missing: "Izbrati morate vsaj {{count}} oznak" + topic_template_not_modified: "Dodajte podrobnosti in značilnosti v vašo temo tako da uredite predlogo teme." save_edit: "Shrani spremembe" overwrite_edit: "Prepiši spremembo" reply_original: "Odgovori v izvorni temi" @@ -1608,6 +1637,7 @@ sl: granted_badge: "Prislužili '{{description}}'" topic_reminder: "{{username}} {{description}}" watching_first_post: "Nova tema {{description}}" + membership_request_accepted: "Sprejeti v članstvo v '{{group_name}}'" group_message_summary: one: "{{count}} sporočilo v{{group_name}} predalu" two: "{{count}} sporočili v {{group_name}} predalu" @@ -1626,7 +1656,24 @@ sl: confirm_body: "Uspelo! Obvestila so bila omogočena." custom: "Obvestilo od {{username}} na %{site_title}" titles: + mentioned: "omenjen" + replied: "nov odgovor" + quoted: "citiran" + edited: "urejen" + liked: "nov všeček" + private_message: "novo zasebno sporočilo" + invited_to_private_message: "povabljen v zasebno sporočilo" + invitee_accepted: "povabilo sprejeto" + posted: "nov prispevek" + moved_post: "prispevek premaknjen" + linked: "povezan" + granted_badge: "značka podeljena" + invited_to_topic: "povabljen v temo" + group_mentioned: "omemba skupine" + group_message_summary: "nova sporočila skupine" watching_first_post: "nova tema" + topic_reminder: "opomnik teme" + liked_consolidated: "novi všečki" post_approved: "prispevek odobren" upload_selector: title: "Dodaj sliko" @@ -1798,6 +1845,8 @@ sl: edit_message: help: "Uredi prvi prispevek sporočila" title: "Uredi sporočilo" + defer: + title: "Preloži" list: "Teme" new: "nova tema" unread: "neprebrana" @@ -1848,6 +1897,10 @@ sl: toggle_information: "preklopi podrobnosti teme" read_more_in_category: "Brskaj po ostalih temah v {{catLink}} ali {{latestLink}}." read_more: "Brskaj po ostalih temah {{catLink}} ali {{latestLink}}." + group_request: "Zaprositi morate članstvo v skupini `{{name}}` za dostop do te teme" + group_join: "Včlaniti se morate v skupino `{{name}}` za dostop do te teme" + group_request_sent: "Vaša prošnja za članstvo je bila poslana. Obveščeni boste, ko bo odobrena." + unread_indicator: "Noben član ni še prebral zadnjega prispevka v tej temi." read_more_MF: "{ UNREAD, plural, =0 {} one { Je še 1 neprebrana } two { Sta še 2 neprebrani } other { Je še# neprebranih } } { NEW, plural, =0 {} one { {BOTH, select, true{in} false {je } other{}} 1 nova tema ostala} two { {BOTH, select, true{in } false {sta } other{}} 2 novi temi ostali} other { {BOTH, select, true{in } false {so } other{}} # nove teme ostale} }, ali {CATEGORY, select, true {brskaj druge teme v {catLink}} false {{latestLink}} other {}}" browse_all_categories: Brskajte po vseh kategorijah view_latest_topics: poglej najnovejše teme @@ -2016,6 +2069,9 @@ sl: title: "Prijavi" help: "prijavi to temo moderatorjem ali pošlji opozorilo avtorju" success_message: "Uspešno ste prijavili to temo." + make_public: + title: "Pretvori v javno temo" + choose_category: "Izberite kategorijo za to javno temo:" feature_topic: title: "Izpostavi to temo" pin: "Naj se ta tema prikaže na vrhu kategorije {{categoryLink}} do" @@ -2237,6 +2293,7 @@ sl: abandon: confirm: "Ali ste prepričani, da hočete zavreči vaš prispevek?" no_value: "Ne, ohrani" + no_save_draft: "Ne, shranite osnutek" yes_value: "Da, zavrži" via_email: "ta prispevek je prispel preko e-sporočila" via_auto_generated_email: "ta prispevek je prispel preko samodejno ustvarjenega e-sporočila" @@ -2250,6 +2307,7 @@ sl: reply: "sestavi odgovor na ta prispevek" like: "všečkaj ta prispevek" has_liked: "všečkali ste prispevek" + read_indicator: "člani, ki so prebrali ta prispevek" undo_like: "razveljavi všeček" edit: "uredi prispevek" edit_action: "Uredi" @@ -2290,6 +2348,7 @@ sl: delete_topic: "izbriši temo" add_post_notice: "Dodaj obvestilo osebja" remove_post_notice: "Odstrani obvestilo osebja" + remove_timer: "odstrani opomnik" actions: flag: "Prijavi" defer_flags: @@ -2311,6 +2370,7 @@ sl: notify_user: "pošlji opozorilo" bookmark: "je zaznamovalo" like: "je všeč" + read: "so prebrali" like_capped: one: "in {{count}} drugemu uporabniku je prispevek všeč" two: "in {{count}} drugima uporabnikoma je prispevek všeč" @@ -2417,6 +2477,7 @@ sl: special_warning: "Ta kategorija je prednastavljena, zato varnostnih nastavitev ni mogoče urejati. Če ne želite uporabljati te kategorije jo raje izbrišite kot uporabljajte v drug namen." uncategorized_security_warning: "Ta kategorija je posebna. Namenjena je za shranjevanje tem, ki nimajo kategorije. Zato ne more imeti varnostnih nastavitev." uncategorized_general_warning: 'Ta kategorija je posebna. Uporabi se kot privzeta kategorija za teme brez kategorije. Če hočete onemogočiti takšen način in hočete obvezno izbiro kategorije, potem onemogočite nastavitev tukaj. Če hočete spremeniti ime ali opis, pojdite Prilagodi / Vsebina besedila.' + pending_permission_change_alert: "Niste dodali %{group} v to kategorijo; kliknite ta gumb za dodajanje." images: "Slike" email_in: "Dohodni e-naslov po meri:" email_in_allow_strangers: "Dovoli e-sporočila od anonimnih uporabnikov brez računa" @@ -2574,6 +2635,8 @@ sl: help: "Ta tema je pripeta; prikazala se bo na vrhu njene kategorije." unlisted: help: "Ta tema je izločena; ne bo se prikazovala na seznamih tem in se jo lahko dostopa samo preko neposredne povezave." + personal_message: + title: "Ta tema je zasebno sporočilo" posts: "Prispevki" posts_long: "{{number}} prispevkov v tej temi" posts_likes_MF: | @@ -2736,6 +2799,7 @@ sl: up_down: "%{shortcut} Prestavi izbiro ↑ ↓" open: "%{shortcut} Odpri izbrano temo" next_prev: "%{shortcut} Naslednja/predhodna sekcija" + go_to_unread_post: "%{shortcut} Na prvi neprebran prispevek" application: title: "Aplikacija" create: "%{shortcut} Ustvari novo temo" @@ -2772,6 +2836,7 @@ sl: mark_tracking: "%{shortcut} Sledi temi" mark_watching: "%{shortcut} Spremljaj temo" print: "%{shortcut} Natisni temo" + defer: "%{shortcut} Preloži temo" badges: earned_n_times: one: "Prislužili to značko %{count} krat" @@ -2886,6 +2951,7 @@ sl: about: "Dodajte oznake v skupine da jih lažje upravljate" new: "Nova skupina" tags_label: "Oznake v tej skupini:" + tags_placeholder: "oznake" parent_tag_label: "Nadrejena oznaka:" parent_tag_placeholder: "Neobvezno" parent_tag_description: "Oznake iz te skupine se ne morejo uporabiti, če ni prisotna tudi nadrejena oznaka." @@ -3073,6 +3139,7 @@ sl: user: "Uporabnik" title: "API" key: "API ključ" + created: Ustvarjeno generate: "Generiraj" regenerate: "Regeneriraj" revoke: "Prekliči" @@ -3428,6 +3495,7 @@ sl: regular: "Uporabniki na nivoju zaupanja 3 (redni)" leader: "Uporabniki na nivoju zaupanja 4 (vodja)" staff: "Osebje" + moderators: "Moderatorji" silenced: "Utišani uporabniki" suspended: "Suspendirani uporabniki" suspect: "Sumljivi uporabniki" diff --git a/config/locales/client.sq.yml b/config/locales/client.sq.yml index 1373e7b601..ac74d9ac9b 100644 --- a/config/locales/client.sq.yml +++ b/config/locales/client.sq.yml @@ -571,7 +571,10 @@ sq: disable: "Çaktivizo" enable: "Aktivizo" second_factor: + name: "Emri" edit: "Redakto" + security_key: + delete: 'Fshij' change_about: title: "Ndrysho Rreth meje" error: "Pati një gabim gjatë ndryshimit të kësaj të dhëne." @@ -913,20 +916,15 @@ sq: google_oauth2: name: "Google" title: "me Google" - message: "Duke u identifikuar me Google (bllokuesit e popup-eve duhet të jenë të çaktivizuar)" twitter: name: "Twitter" title: "me Twitter" - message: "Duke u identifikuar me Twitter (çaktivizoni bllokuesit e popupeve, nëse i përdorni)" instagram: title: "me Instagram" - message: "Duke u identifikuar me Instagram (bllokuesit e popup-eve duhet të jenë të çaktivizuar)" facebook: title: "me Facebook" - message: "Duke u identifikuar me Facebook (çaktivizoni bllokuesit e popupeve, nëse i përdorni)" github: title: "me GitHub" - message: "Duke u identifikuar me Github (bllokuesit e popup-eve duhet të jenë të çaktivizuar)" invites: accept_title: "Ftesë" welcome_to: "Mirë se vini tek %{site_name}!" @@ -943,7 +941,6 @@ sq: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" category_page_style: categories_only: "Vetëm kategoritë" @@ -2031,6 +2028,7 @@ sq: user: "Përdorues" title: "API" key: "API Key" + created: Krijuar generate: "Gjenero" regenerate: "Rigjenero" revoke: "Revoko" diff --git a/config/locales/client.sr.yml b/config/locales/client.sr.yml index 01412860e3..0a95b73b3d 100644 --- a/config/locales/client.sr.yml +++ b/config/locales/client.sr.yml @@ -519,7 +519,10 @@ sr: disable: "Onemogući" enable: "Omogući" second_factor: + name: "Ime foruma" edit: "Izmeni" + security_key: + delete: 'Obriši' change_about: title: "Promijeni O meni" change_username: @@ -825,17 +828,13 @@ sr: google_oauth2: name: "Google" title: "pomoću Google-a" - message: "Autentifikacija pomoću Googlea (isključite pop-up blokere)" twitter: name: "Twitter" title: "pomoću Twitter-a" - message: "Autentifikacija preko Twittera (isključite pop-up blokere)" facebook: title: "pomoću Facebook-a" - message: "Autentifikacija preko Facebooka (isključite pop-up blokere)" github: title: "pomoću GitHuba" - message: "Autentifikacija preko GitHuba (isključite pop-up blokere)" invites: welcome_to: "Dobrodošao na %{site_name}!" success: "Tvoj nalog je kreiran i sada si ulogovan." @@ -845,7 +844,6 @@ sr: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" shortcut_modifier_key: alt: "Alt" conditional_loading_section: @@ -1657,6 +1655,7 @@ sr: user: "Korisnik" title: "API" key: "API Ključ" + created: Napravljeno generate: "Generiši" regenerate: "Regeneriši" revoke: "Povuci" diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml index 96168c2d68..7891c623c5 100644 --- a/config/locales/client.sv.yml +++ b/config/locales/client.sv.yml @@ -621,7 +621,11 @@ sv: disable: "Inaktivera" enable: "Aktivera" second_factor: + name: "Namn" edit: "Redigera" + security_key: + register: "Registrera" + delete: 'Radera' change_about: title: "Ändra Om Mig" error: "Ett fel inträffade vid ändringen av det här värdet." @@ -968,20 +972,15 @@ sv: google_oauth2: name: "Google" title: "med Google" - message: "Autentiserar med Google (kolla så att pop up-blockare inte är aktiverade)" twitter: name: "Twitter" title: "med Twitter" - message: "Autentiserar med Twitter (kolla så att pop up-blockare inte är aktiverade)" instagram: title: "med Instagram" - message: "Autentisering med Instagram (se till att popup-blockeringar inte är aktiverade)" facebook: title: "med Facebook" - message: "Autentiserar med Facebook (kolla så att pop up-blockare inte är aktiverade)" github: title: "med GitHub" - message: "Autentiserar med GitHub (kolla så att pop up-blockare inte är aktiverade)" invites: accept_title: "Inbjudan" welcome_to: "Välkommen till %{site_name}!" @@ -998,7 +997,6 @@ sv: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" category_page_style: categories_only: "Endast kategorier" @@ -2181,6 +2179,7 @@ sv: user: "Användare" title: "API" key: "API-nyckel" + created: Skapad generate: "Generera" regenerate: "Regenerera" revoke: "Återkalla" diff --git a/config/locales/client.sw.yml b/config/locales/client.sw.yml index 4e19c155d5..a46b39be55 100644 --- a/config/locales/client.sw.yml +++ b/config/locales/client.sw.yml @@ -690,11 +690,15 @@ sw: second_factor: title: "Uhalalalishaji wa Viwango Viwili" confirm_password_description: "Thibitisha nywila yako kuendelea" + name: "Jina" label: "Kodi" disable_description: "Tafadhali andika kodi ya uthibitisho kutoka kwenye app yako" show_key_description: "Andika kwa mkono" oauth_enabled_warning: "Tafadhali jua kuwa kuingia kupitia mitandao itasitishwa kama uthibitisho wa kiwango cha pili umewezeshwa kwenye akaunti yako." edit: "Hariri" + security_key: + register: "Jisajili" + delete: 'Futa' change_about: title: "Badilisha Taarifa Zangu" error: "Hitilafu imetokea wakati wa kubadilisha namba hii." @@ -1044,10 +1048,8 @@ sw: password: "Nywila" second_factor_title: "Uhalalalishaji wa Viwango Viwili" second_factor_description: "Tafadhali andika kodi ya uthibitisho kutoka kwenye app yako:" - second_factor_backup: "Ingia kutumia kodi ya backup " second_factor_backup_title: "Backup kutumia steji mbili" second_factor_backup_description: "Samahani, Ingiza mojawapo ya kodi yako ya backup" - second_factor: "Ingia kutumia App ya Uthibitisho " email_placeholder: "barua pepe au jina la mtumiaji" caps_lock_warning: "Caps Lock imewashwa" error: "Tatizo lilisojulikana" @@ -1078,23 +1080,18 @@ sw: google_oauth2: name: "Google" title: "na Google" - message: "uthibitisho na Google (hakikisha vizuizi vya pop-up havijaruhusiwa)" twitter: name: "Twitter" title: "na Twitter" - message: "uthibitisho na Twitter (hakikisha vizuizi vya pop-up havijaruhusiwa)" instagram: name: "Instagram" title: "na Instagram" - message: "uthibitisho na Instagram (hakikisha vizuizi vya pop-up havijaruhusiwa)" facebook: name: "Facebook" title: "na Facebook" - message: "uthibitisho na Facebook (hakikisha vizuizi vya pop-up havijaruhusiwa)" github: name: "GitHub" title: "na Github" - message: "uthibitisho na Github (hakikisha vizuizi vya pop-up havijaruhusiwa)" discord: name: "Matatizo" invites: @@ -1114,7 +1111,6 @@ sw: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Ishara ya Kwanza" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -2441,6 +2437,7 @@ sw: user: "Mtumiaji" title: "API" key: "Ufunguo wa API" + created: Imetengenezwa generate: "Tengeneza" regenerate: "Tengeneza Upya" revoke: "Futa" diff --git a/config/locales/client.te.yml b/config/locales/client.te.yml index 7dbfe03879..5f3216bf50 100644 --- a/config/locales/client.te.yml +++ b/config/locales/client.te.yml @@ -414,7 +414,10 @@ te: disable: "అచేతనం" enable: "చేతనం" second_factor: + name: "పేరు" edit: "సవరించు" + security_key: + delete: 'తొలగించు' change_about: title: "నా గురించి మార్చు" change_username: @@ -649,17 +652,13 @@ te: google_oauth2: name: "గూగుల్" title: "గూగుల్ తో" - message: "గూగుల్ ద్వారా లాగిన్ (పాపప్ లు అనుమతించుట మర్చిపోకండి)" twitter: name: "ట్విట్టర్" title: "ట్విట్టరు తో" - message: "ట్విట్టరు ద్వారా లాగిన్ (పాపప్ లు అనుమతించుట మర్చిపోకండి)" facebook: title: "ఫేస్ బుక్ తో" - message: "ఫేస్ బుక్ ద్వారా లాగిన్ (పాపప్ లు అనుమతించుట మర్చిపోకండి)" github: title: "గిట్ హబ్ తో" - message: "గిట్ హబ్ ద్వారా లాగిన్ (పాపప్ లు అనుమతించుట మర్చిపోకండి)" invites: accept_title: "ఆహ్వానం" welcome_to: "%{site_name} కు సుస్వాగతం!" @@ -672,7 +671,6 @@ te: apple_international: "యాపిల్ , అంతర్జాతీయ" google: "గూగుల్" twitter: "ట్విట్టర్" - emoji_one: "ఇమెజి వన్" category_page_style: categories_only: "వర్గాలు మాత్రమే" conditional_loading_section: @@ -1402,6 +1400,7 @@ te: user: "సభ్యుడు" title: "ఏపీఐ" key: "ఏపీఐ కీ" + created: సృష్టించిన generate: "ఉత్పత్తించు" regenerate: "పునరుత్పత్తించు" revoke: "రివోక్" diff --git a/config/locales/client.th.yml b/config/locales/client.th.yml index c00c4ac25a..0d6372af46 100644 --- a/config/locales/client.th.yml +++ b/config/locales/client.th.yml @@ -541,7 +541,10 @@ th: disable: "ปิดใช้งาน" enable: "เปิดใช้งาน" second_factor: + name: "ชื่อ" edit: "แก้ไข" + security_key: + delete: 'ลบ' change_about: title: "เปลี่ยนข้อมูลเกี่ยวกับฉัน" error: "เกิดความผิดพลาดในการแก้ไขค่านี้" @@ -863,20 +866,15 @@ th: google_oauth2: name: "กูเกิ้ล" title: "ด้วย Google" - message: "กำลังตรวจสอบกับ Google (ต้องไม่ปิดกั้นป๊อปอัพ)" twitter: name: "ทวิตเตอร์" title: "ด้วย Twitter" - message: "กำลังตรวจสอบกับ Twitter (ต้องไม่ปิดกั้นป๊อปอัพ)" instagram: title: "ด้วย Instragram" - message: "เข้าใช้งานด้วย Instragram (โปรดแน่ใจว่าไม่ได้เปิดใช้งานการป้องกัน pop up)" facebook: title: "ด้วย Facebook" - message: "กำลังตรวจสอบกับ Facebook (ต้องไม่ปิดกั้นป๊อปอัพ)" github: title: "ด้วย GitHub" - message: "กำลังตรวจสอบกับ Github (ต้องไม่ปิดกั้นป๊อปอัพ)" invites: name_label: "ชื่อ" password_label: "ตั้งรหัสผ่าน" @@ -884,7 +882,6 @@ th: apple_international: "แอปเปิ้ล/นานาชาติ" google: "กูเกิ้ล" twitter: "ทวิตเตอร์" - emoji_one: "Emoji One" shortcut_modifier_key: shift: "Shift" ctrl: "Ctrl" @@ -1595,6 +1592,7 @@ th: user: "ผู้ใช้" title: "API" key: "API Key" + created: สร้างเมื่อ revoke: "เอาออก" web_hooks: save: "บันทึก" diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index 217db73609..07e8cd3650 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -856,7 +856,6 @@ tr_TR: copied_to_clipboard: "Panoya kopyalandı" copy_to_clipboard_error: "Panoya kopyalanırken hata oluştu" remaining_codes: "{{count}} yedek kodun kaldı " - use: "Yedek kod kullan" enable_prerequisites: "Yedek kodları oluşturmadan önce birincil ikinci faktörü etkinleştirmelisiniz." codes: title: "Yedek kod oluşturuldu" @@ -865,6 +864,7 @@ tr_TR: title: "İki Faktörlü Kimlik Doğrulama" enable: "İki Adımlı Kimlik Doğrulamayı Düzenle" confirm_password_description: "Devam etmek için lütfen şifrenizi onaylayın" + name: "İsim" label: "Kod" rate_limit: "Yeni bir doğrulama kodu girmeden önce lütfen bekleyin." enable_description: | @@ -876,7 +876,6 @@ tr_TR: extended_description: | İki faktörlü kimlik doğrulama, şifrenize ek olarak bir kerelik bir belirteç gerektirerek hesabınıza ekstra güvenlik sağlar. Tokenler Android ve iOS cihazlarda yaratılabilir. oauth_enabled_warning: "Hesabınızda iki faktörlü kimlik doğrulaması etkinleştirildikten sonra sosyal girişlerin devre dışı bırakılacağını lütfen unutmayın." - use: "Authenticator uygulamasını kullan" enforced_notice: "Bu siteye erişmeden önce iki faktörlü kimlik doğrulamasını etkinleştirmeniz gerekir." disable: "devre dışı bırak" disable_title: "İkinci Faktör İnaktif" @@ -888,6 +887,9 @@ tr_TR: title: "Token Tabanlı Doğrulayıcılar" add: "Yeni Doğrulayıcı" default_name: "Benim Doğrulayıcım" + security_key: + register: "Kayıt Ol" + delete: 'Sil' change_about: title: "\"Hakkımda\"yı Değiştir" error: "Bu değeri değiştirirken bir hata oluştu." @@ -1281,10 +1283,8 @@ tr_TR: password: "Şifre" second_factor_title: "İki Faktörlü Kimlik Doğrulama" second_factor_description: "Lütfen uygulamadan \"Kimlik Doğrulama Kodu\"nu gir:" - second_factor_backup: "Yedek kodları kullanarak oturum açın" second_factor_backup_title: "İki Faktörlü Yedekleme" second_factor_backup_description: "Lütfen yedek kodlarından birini gir:" - second_factor: "Authenticator uygulamasını kullanarak oturum açın" email_placeholder: "e-posta veya kullanıcı adı" caps_lock_warning: "Caps Lock açık" error: "Bilinmeyen hata" @@ -1317,27 +1317,21 @@ tr_TR: google_oauth2: name: "Google" title: "Google ile" - message: "Google ile kimlik doğrulaması yapılıyor (pop-up engelleyicilerin etkinleştirilmediğinden emin ol)" twitter: name: "Twitter" title: "Twitter ile" - message: "Twitter ile kimlik doğrulaması yapılıyor (pop-up engelleyicilerin etkinleştirilmediğinden emin ol)" instagram: name: "Instagram" title: "Instagram ile" - message: "Instagram ile kimlik doğrulaması yapılıyor (pop-up engelleyicilerin etkinleştirilmediğinden emin ol)" facebook: name: "Facebook" title: "Facebook ile" - message: "Facebook ile kimlik doğrulaması yapılıyor (pop-up engelleyicilerin etkinleştirilmediğinden emin ol)" github: name: "GitHub" title: "GitHub ile" - message: "GitHub ile kimlik doğrulaması yapılıyor (pop-up engelleyicilerin etkinleştirilmediğinden emin ol)" discord: name: "Discord" title: "Discord ile" - message: "Discord ile Kimlik Doğrulama" invites: accept_title: "Davet" welcome_to: "%{site_name} hoş geldin!" @@ -1355,7 +1349,6 @@ tr_TR: apple_international: "Apple/Uluslararası" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -2926,6 +2919,7 @@ tr_TR: user: "Kullanıcı" title: "API" key: "API Anahtarı" + created: Oluşturuldu generate: "Oluştur" regenerate: "Tekrar Oluştur" revoke: "İptal Et" diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml index 7a84019f06..b45f838b2f 100644 --- a/config/locales/client.uk.yml +++ b/config/locales/client.uk.yml @@ -932,7 +932,6 @@ uk: copied_to_clipboard: "Скопійовано у буфер обміну" copy_to_clipboard_error: "Помилка під час копіювання даних у буфер обміну" remaining_codes: "У вас залишилося резервних кодів: {{count}}." - use: "Використати резервний код" enable_prerequisites: "Ви повинні увімкнути основний другий фактор, перш ніж генерувати резервні коди." codes: title: "Резервні коди згенеровано" @@ -941,6 +940,7 @@ uk: title: "Двофакторна автентифікація" enable: "Керувати двофакторною автентифікацією" confirm_password_description: "Будь ласка, підтвердіть свій пароль для продовження" + name: "Ім'я" label: "Код" rate_limit: "Будь ласка, зачекайте, перш ніж пробувати наступний код автентифікації." enable_description: | @@ -952,7 +952,6 @@ uk: extended_description: | Двофакторна автентифікація краще захищає ваш обліковий запис, вимагаючи окрім вашого пароля ще й введення одноразового токена. Токени можна генерувати на пристроях з Android та iOS. oauth_enabled_warning: "Будь ласка, зверніть увагу, що вхід через соціальні мережі буде вимкнено, як тільки у вашому обліковому записі буде увімкнена двофакторна автентифікація." - use: "Скористатися програмкою-автентифікатором" enforced_notice: "Ви мусите увімкнути двофакторну автентифікацію, щоб отримати доступ до цього сайту." disable: "вимкнути" disable_title: "Вимкнути другий фактор" @@ -964,6 +963,8 @@ uk: title: "Автентифікатори на токенах" add: "Новий автентифікатор" default_name: "Мій автентифікатор" + security_key: + delete: 'Вилучити' change_about: title: "Змінити Про мене" error: "Сталася помилка під час цієї зміни." @@ -1214,21 +1215,16 @@ uk: google_oauth2: name: "Google" title: "з Google" - message: "Автентифікація через Google (перевірте, щоб блокувальники спливних вікон були вимкнені)" twitter: name: "Twitter" title: "через Twitter" - message: "Автентифікація через Twitter (перевірте, щоб блокувальники спливних вікон були вимкнені)" instagram: name: "Instagram" title: "через Instagram" - message: "Автентифікація через Instagram (переконайтеся, що у вас дозволені спливні вікна)" facebook: title: "через Facebook" - message: "Автентифікація через Facebook (перевірте, щоб блокувальники спливних вікон були вимкнені)" github: title: "через GitHub" - message: "Автентифікація через GitHub (перевірте, щоб блокувальники спливних вікон були вимкнені)" invites: welcome_to: "Ласкаво просимо до сайта %{site_name}!" name_label: "Ім'я" @@ -1979,6 +1975,7 @@ uk: user: "Користувач" title: "API" key: "Ключ API" + created: Створено generate: "Згенерувати" regenerate: "Перегенерувати" revoke: "Анулювати" diff --git a/config/locales/client.ur.yml b/config/locales/client.ur.yml index 543823e7a8..d09edc0e4c 100644 --- a/config/locales/client.ur.yml +++ b/config/locales/client.ur.yml @@ -880,7 +880,6 @@ ur: copied_to_clipboard: "کلِپ بورڈ میں کاپی کر لیا گیا" copy_to_clipboard_error: "کلِپ بورڈ میں ڈیٹا کاپی کرنے پر خرابی کا سامنا کر نا پرا" remaining_codes: "آپ کے پاس {{count}} بیک اپ کوڈ باقی ہیں۔" - use: "ایک بیک اپ کوڈ استعمال کریں" enable_prerequisites: "بیک اپ کوڈز تیار کرنے سے پہلے آپ کا ایک بنیادی دوسرا فیکٹر فعال کرنا ضروری ہے۔" codes: title: "بیک اپ کوڈز تیار" @@ -889,6 +888,7 @@ ur: title: "دو فیکٹر توثیق" enable: "دو فیکٹر توثیق انتظام کریں" confirm_password_description: "جاری رکھنے کیلئے براہ کرم اپنے پاسوَرڈ کی تصدیق کریں" + name: "نام" label: "کَوڈ" rate_limit: "دوبارہ توثیقی کوڈ کی کوشش کرنے سے پہلے براہ کرم تھوڑا انتظار کریں۔" enable_description: | @@ -900,7 +900,6 @@ ur: extended_description: | دو فیکٹر توثیق پاسوَرڈ کے ساتھ ساتھ ایک دفعہ کے ٹَوکن کو مانگ کر آپ کے اکاؤنٹ کیلئے اضافی سیکورٹی شامل کرتا ہے۔ ٹَوکن اینڈرائڈ اور iOS ڈیوائسوں پر تخلیق کیے جا سکتے ہیں۔ oauth_enabled_warning: "براہ مہربانی نوٹ کریں کہ ایک بار جب آپ کے اکاؤنٹ پر دو فیکٹر توثیق فعال ہو جائے تو سَوشَل لاگ اِن غیر فعال ہوجائیں گے۔" - use: "اَوتھینٹیکَیٹر اَیپ کا استعمال کریں" enforced_notice: "اِس سائٹ تک رسائی حاصل کرنے کیلئے آپ کا دو فیکٹر توثیق فعال کرنا ضروری ہے۔" disable: "غیر فعال" disable_title: "دوسرا فیکٹر غیر فعال کریں" @@ -912,6 +911,9 @@ ur: title: "ٹوکن کی بنیاد پر تصديق کنندہ" add: "نیا تصديق کنندہ" default_name: "میرا تصديق کنندہ" + security_key: + register: "رجسٹر" + delete: 'حذف کریں' change_about: title: "\"میرے بارے میں\" تبدیل کریں" error: "اِس چیز کو تبدیل کرنے میں ایک خرابی کا سامنا کرنا پڑا۔" @@ -1309,10 +1311,8 @@ ur: password: "پاسورڈ" second_factor_title: "دو فیکٹر توثیق" second_factor_description: "براہ مہربانی اپنی اَیپ میں سے توثیقی کَوڈ درج کریں:" - second_factor_backup: "بیک اپ کوڈ کا استعمال کرتے ہوئے لاگ اِن کریں" second_factor_backup_title: "دو فیکٹر بیک اپ" second_factor_backup_description: "براہ مہربانی اپنے بیک اپ کوڈز میں سے ایک درج کریں:" - second_factor: "اَوتھینٹیکَیٹر اَیپ کا استعمال کرتے ہوئے لاگ اِن کریں" email_placeholder: "اِی میل یا صارف نام" caps_lock_warning: "کیپس لاک آن ہے" error: "نامعلوم خرابی" @@ -1345,27 +1345,24 @@ ur: google_oauth2: name: "گُوگَل" title: "گوگل سے" - message: "گُوگَل کے زریعے تصدیق کی جا رہی ہے (یقینی بنائیں کہ پاپ اَپ بلاکرز فعال نہیں ہیں)" twitter: name: "Twitter" title: "ٹویٹر سے" - message: "ٹویٹر کے زریعے تصدیق کی جا رہی ہے (یقینی بنائیں کہ پاپ اَپ بلاکرز فعال نہیں ہیں)" instagram: name: "اِنسٹاگرام" title: "اِنسٹاگرام سے" - message: "اِنسٹاگرام کے زریعے تصدیق کی جا رہی ہے (یقینی بنائیں کہ پاپ اَپ بلاکرز فعال نہیں ہیں)" facebook: name: "فَیسبُک" title: "فیس بک سے" - message: "گُوگَلفیس بک کے زریعے تصدیق کی جا رہی ہے (یقینی بنائیں کہ پاپ اَپ بلاکرز فعال نہیں ہیں)" github: name: "گِٹ ہَب" title: "گِٹ ہَب سے" - message: "گِٹ ہَب کے زریعے تصدیق کی جا رہی ہے (یقینی بنائیں کہ پاپ اَپ بلاکرز فعال نہیں ہیں)" discord: name: "ڈِسکَورڈ" title: "ڈِسکَورڈ کے ساتھ" - message: "ڈِسکَورڈ کے ساتھ تصدیق" + second_factor_toggle: + totp: "بجائے ایک اَوتھینٹیکَیٹر اَیپ کا استعمال کریں" + backup_code: "بجائے ایک بیک اپ کوڈ استعمال کریں" invites: accept_title: "دعوت نامہ" welcome_to: "%{site_name} پر خوش آمدید!" @@ -1383,7 +1380,6 @@ ur: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "گُوگَل کلاسِک" facebook_messenger: "فَیس بُک مَیسنجر" @@ -3022,6 +3018,7 @@ ur: user: "صارف" title: "API" key: "API کِی" + created: بنایا گیا generate: "تخلیق کریں" regenerate: "دوبارہ تخلیق کریں" revoke: "منسوخ کریں" @@ -3344,7 +3341,6 @@ ur: other: "تِھیم {{count}} کَمِٹس پیچھے ہے!" compare_commits: "(نئے کَمِٹس دیکھیں)" repo_unreachable: "اِس تِھیم کی گِٹ رِیپَوزِٹَری سے رابطہ نہیں کیا جا سکا۔ تکنیکی خرابی کا پیغام:" - imported_from_archive: "یہ تِھیم ایک .tar.gz فائل سے درآمد کی گئی تھی" scss: text: "CSS" title: "اپنی مرضی کے مطابق CSS درج کریں، ہم تمام درست CSS اور SCSS سٹائلز قبول کرتے ہیں" diff --git a/config/locales/client.vi.yml b/config/locales/client.vi.yml index 8b744a574d..8daaec0f8d 100644 --- a/config/locales/client.vi.yml +++ b/config/locales/client.vi.yml @@ -772,11 +772,13 @@ vi: copied_to_clipboard: "Sao chép vào Clipboard" copy_to_clipboard_error: "Lỗi sao chép dữ liệu vào Clipboard" remaining_codes: "Bạn có {{count}} mã sao lưu còn lại." - use: "Sử dụng một mã sao lưu" second_factor: title: "Xác minh hai bước" + name: "Tên" show_key_description: "Nhập thủ công" edit: "Sửa" + security_key: + delete: 'Xóa' change_about: title: "Thay đổi thông tin về tôi" error: "Có lỗi xảy ra khi thay đổi giá trị này." @@ -1122,7 +1124,6 @@ vi: password: "Mật khẩu" second_factor_title: "Xác minh hai bước" second_factor_description: "Vui lòng nhập mã xác minh từ ứng dụng của bạn:" - second_factor_backup: "Đăng nhập sử dụng mã sao lưu" email_placeholder: "Email hoặc tên đăng nhập " caps_lock_warning: "Phím Caps Lock đang được bật" error: "Không xác định được lỗi" @@ -1151,23 +1152,18 @@ vi: google_oauth2: name: "Goole" title: "với Google" - message: "Chứng thực với Google (chắc chắn rằng cửa sổ pop up blocker không được kích hoạt)" twitter: name: "Twitter" title: "với Twitter" - message: "Chứng thực với Twitter(hãy chắc chắn là chăn pop up không bật)" instagram: name: "Instagram" title: "với Instagram" - message: "Chứng thực với Instagram (chăc chắn rằng chặn pop-up không bật)" facebook: name: "Facebook" title: "với Facebook" - message: "Chứng thực với Facebook(chắc chắn là chặn pop up không bật)" github: name: "GitHub" title: "với GitHub" - message: "Chứng thực với GitHub (chắc chắn chặn popup không bật)" invites: accept_title: "Lời mời" welcome_to: "Chào mừng bạn đến với %{site_name}!" @@ -1183,7 +1179,6 @@ vi: apple_international: "Apple/International" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -2266,6 +2261,7 @@ vi: user: "Thành viên" title: "API" key: "API Key" + created: Tạo bởi generate: "Khởi tạo" regenerate: "Khởi tạo lại" revoke: "Thu hồi" diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index 68c32d4820..40b630f763 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -830,7 +830,6 @@ zh_CN: copied_to_clipboard: "已复制到剪贴板" copy_to_clipboard_error: "复制到剪贴板时出错" remaining_codes: "你有{{count}}个备份码" - use: "使用备份码" enable_prerequisites: "你必须在生成备份代码之前启用主要第二因素。" codes: title: "备份码生成" @@ -839,6 +838,7 @@ zh_CN: title: "双重验证" enable: "管理两步验证" confirm_password_description: "确认密码以继续" + name: "名称" label: "编码" rate_limit: "请等待另一个验证码。" enable_description: | @@ -850,7 +850,6 @@ zh_CN: extended_description: | 双重身份验证除了你的密码之外还需要一次性令牌,从而为你的帐户增加了额外的安全性。 可以在AndroidiOS设备。 oauth_enabled_warning: "请注意,一旦你的帐户启用了双重身份验证,系统就会停用社交登录。" - use: "Authenticator app" enforced_notice: "在访问此站点之前,你需要启用双重身份验证。" disable: "停用" disable_title: "禁用次要身份验证器" @@ -862,6 +861,9 @@ zh_CN: title: "基于凭证的身份验证器" add: "新增身份验证器" default_name: "我的身份验证器" + security_key: + register: "注册" + delete: '删除' change_about: title: "更改个人信息" error: "提交修改时出错了" @@ -1250,10 +1252,8 @@ zh_CN: password: "密码" second_factor_title: "双重验证" second_factor_description: "请输入来自 app 的验证码:" - second_factor_backup: "使用备用码登录" second_factor_backup_title: "两步验证备份" second_factor_backup_description: "请输入你的备份码:" - second_factor: "使用Authenticator app登录" email_placeholder: "电子邮件或者用户名" caps_lock_warning: "大写锁定开启" error: "未知错误" @@ -1286,26 +1286,23 @@ zh_CN: google_oauth2: name: "Google" title: "Google 登录" - message: "正在通过 Google 帐号验证登录(请确保浏览器没有禁止弹出窗口)" twitter: name: "Twitter" title: "Twitter 登录" - message: "正在通过 Twitter 帐号验证登录(请确保浏览器没有禁止弹出窗口)" instagram: name: "Instagram" title: "Instagram 登录" - message: "正在通过 Instagram 帐号验证登录(请确保浏览器没有禁止弹出窗口)" facebook: name: "Facebook" title: "Facebook 登录" - message: "正在通过 Facebook 帐号验证登录(请确保浏览器没有禁止弹出窗口)" github: name: "GitHub" title: "GitHub 登录" - message: "正在通过 GitHub 帐号验证登录(请确保浏览器没有禁止弹出窗口)" discord: name: "Discord" - message: "使用Discord验证" + second_factor_toggle: + totp: "改用身份验证APP" + backup_code: "使用备份码" invites: accept_title: "邀请" welcome_to: "欢迎来到%{site_name}!" @@ -1323,7 +1320,6 @@ zh_CN: apple_international: "Apple/国际化" google: "Google" twitter: "Twitter" - emoji_one: "Emoji One" win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -2899,6 +2895,7 @@ zh_CN: user: "用户" title: "API" key: "API 密钥" + created: 创建时间 generate: "生成" regenerate: "重新生成" revoke: "撤销" @@ -3218,7 +3215,6 @@ zh_CN: other: "主题落后了 {{count}} 个变更!" compare_commits: "(查看新提交)" repo_unreachable: "无法联系此主题的Git存储库。错误信息:" - imported_from_archive: "此主题是从.tar.gz文件导入的" scss: text: "CSS" title: "输入自定义 CSS,我们接受所有有效的 CSS 和 SCSS 样式" diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml index 13a7ad64d4..c875f7ff22 100644 --- a/config/locales/client.zh_TW.yml +++ b/config/locales/client.zh_TW.yml @@ -798,13 +798,13 @@ zh_TW: copied_to_clipboard: "複製至剪貼簿" copy_to_clipboard_error: "複製至剪貼簿時發生錯誤" remaining_codes: "你還剩下 {{count}} 個可使用的備用碼" - use: "使用備用碼" codes: title: "已產生備用碼" description: "每個備用碼僅可使用一次。請將它們保存在安全且可被找到的地方。" second_factor: title: "兩步驟驗證" confirm_password_description: "請確認你的密碼以繼續" + name: "名稱" label: "代碼" rate_limit: "嘗試其他驗證代碼前請稍後" enable_description: | @@ -816,9 +816,11 @@ zh_TW: extended_description: | 二步驟驗證使用除了密碼外的一次性代碼來加強帳號安全性。可以使用AndroidiOS設備來產生代碼。 oauth_enabled_warning: "請注意,一旦啟用二步驟驗證,將自動關閉社群登入。" - use: "使用身分驗證應用程式" enforced_notice: "在進入網站前,需啟用兩個步驟驗證以增強帳號安全性" edit: "編輯" + security_key: + register: "註冊" + delete: '刪除' change_about: title: "修改關於我" error: "修改設定值時發生錯誤" @@ -1195,10 +1197,8 @@ zh_TW: password: "密碼" second_factor_title: "兩步驟驗證" second_factor_description: "請輸入應用程式中的驗證碼:" - second_factor_backup: "使用備用碼登入" second_factor_backup_title: "兩步驟驗證備用" second_factor_backup_description: "請輸入一組您的備用碼" - second_factor: "使用身分驗證應用程式登入" email_placeholder: "電子郵件地址或使用者名稱" caps_lock_warning: "大寫鎖定中" error: "未知的錯誤" @@ -1230,23 +1230,21 @@ zh_TW: google_oauth2: name: "Google" title: "使用 Google 帳號" - message: "使用 Google 帳號認證 ( 請確定你的網頁瀏覽器不會阻擋彈出視窗 )" twitter: name: "Twitter" title: "使用 Twitter" - message: "使用 Twitter 認証 (請確定你的網頁瀏覽器未阻擋彈出視窗)" instagram: name: "Instagram" title: "用 Instagram 登入" - message: "正在通過 Instagram 帳號驗證登入(請確保瀏覽器沒有阻擋快顯視窗)" facebook: name: "Facebook" title: "使用 Facebook" - message: "使用 Facebook 認証 (請確定你的網頁瀏覽器未阻擋彈出視窗)" github: name: "GitHub" title: "使用 GitHub" - message: "使用 GitHub 認証 (請確定你的網頁瀏覽器未阻擋彈出視窗)" + second_factor_toggle: + totp: "請改用身份驗證應用程式" + backup_code: "請改用備用碼" invites: accept_title: "邀請函" welcome_to: "歡迎來到 %{site_name}!" @@ -1264,7 +1262,6 @@ zh_TW: apple_international: "Apple/國際化" google: "Google" twitter: "Twitter" - emoji_one: " Emoji One " win10: "Win10" google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" @@ -2781,6 +2778,7 @@ zh_TW: user: "使用者" title: "API" key: "API 金鑰" + created: 已建立 generate: "產生" regenerate: "重新產生" revoke: "撤銷" @@ -3093,7 +3091,6 @@ zh_TW: other: "佈景主題落後了{{count}}個變更!" compare_commits: "(查看新的提交)" repo_unreachable: "無法連線至此佈景主題的 Git 版本庫。錯誤訊息:" - imported_from_archive: "此佈景主題是從 .tar.gz 檔輸入的。" scss: text: "CSS" title: "輸入自訂的 CSS,可接受所有有效的 CSS 及 SCSS。" diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index 41b6b045c8..a3e5bd858a 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -139,6 +139,8 @@ ar: not_found: "تعذّر العثور على الرّابط أو المورد المطلوب." invalid_access: "ليس مسموحًا لك عرض المورد المطلوب." read_only_mode_enabled: "الموقع في وضع القراءة فقط. عُطّلت التفاعلات." + not_in_group: + join_group: "انضم للمجموعة" reading_time: "وقت القراءة" likes: "الإعجابات" too_many_replies: diff --git a/config/locales/server.be.yml b/config/locales/server.be.yml index 237dd11792..b47d4b32b7 100644 --- a/config/locales/server.be.yml +++ b/config/locales/server.be.yml @@ -1475,7 +1475,6 @@ be: content_matches_auto_block_regex: "Змест адпавядае аўтаматычнай блакаванні рэгулярных выразаў" username: too_long: "занадта доўга" - characters: "павінен ўтрымліваць толькі лічбы, літары, працяжнік і падкрэслення" unique: "павінен быць унікальным" blank: "павінны прысутнічаць" must_begin_with_alphanumeric_or_underscore: "павінен пачынацца з літары, лічбы ці падкрэслівання" diff --git a/config/locales/server.bg.yml b/config/locales/server.bg.yml index 1677631a87..bbe48203c8 100644 --- a/config/locales/server.bg.yml +++ b/config/locales/server.bg.yml @@ -99,6 +99,8 @@ bg: not_found: "Заявеният адрес или ресурс не може да бъде намерен." invalid_access: "Вие нямате права да разглеждате този ресурс." read_only_mode_enabled: "Сайтът в режим само за четене. Действията са непозволени." + not_in_group: + join_group: "Присъедини се към група" reading_time: "Време за четене" likes: "Харесвания" too_many_replies: diff --git a/config/locales/server.bs_BA.yml b/config/locales/server.bs_BA.yml index dde89c78cf..15d89c4136 100644 --- a/config/locales/server.bs_BA.yml +++ b/config/locales/server.bs_BA.yml @@ -117,6 +117,8 @@ bs_BA: not_enough_space_on_disk: "There is not enough space on disk to upload this backup." not_logged_in: "You need to be logged in to do that." read_only_mode_enabled: "The site is in read only mode. Interactions are disabled." + not_in_group: + join_group: "Pridruži se grupi" likes: "Sviđanja" embed: start_discussion: "Start Discussion" diff --git a/config/locales/server.ca.yml b/config/locales/server.ca.yml index a01e63f1ea..8037e5dc13 100644 --- a/config/locales/server.ca.yml +++ b/config/locales/server.ca.yml @@ -208,6 +208,8 @@ ca: invalid_grant_badge_reason_link: "En el motiu de la insígnia no es permeten enllaços externs o enllaços no vàlids a Discourse." email_template_cant_be_modified: "Aquesta plantilla de correu no es pot modificar." invalid_whisper_access: "O bé els xiuxiuejos no estan activats o bé no teniu permís per a crear-ne." + not_in_group: + join_group: "Uneix-te al grup" reading_time: "Temps de lectura." likes: "'M'agrada'" too_many_replies: @@ -373,7 +375,7 @@ ca: sequential_replies: | ### Considereu respondre a diverses publicacions d'una tirada - En lloc web d'escriure unes quantes respostes a un sol tema, considereu escriure una sola resposta que citi diverses publicacions prèvies o faci mencions via @nom. + En comptes d'escriure unes quantes respostes a un sol tema, considereu escriure una sola resposta que citi diverses publicacions prèvies o faci mencions via @nom. Podeu editar la vostra darrera resposta per a afegir una citació. Per a fer-ho, seleccioneu text i premeu el botó de cita resposta que apareixerà. @@ -2022,7 +2024,6 @@ ca: short: "ha de tenir almenys %{min} caràcters" long: "no ha de ser més llarg de %{max} caràcters" too_long: "és massa llarg" - characters: "ha d'incloure solament números, lletres, guions i guions baixos" unique: "ha de ser únic" blank: "ha de ser-hi present" must_begin_with_alphanumeric_or_underscore: "ha de començar amb una lletra, una xifra o un guió baix" @@ -2347,7 +2348,7 @@ ca: text_body_template: | Ho sentim, però el vostre missatge de correu a %{destination} (titulat %{former_title}) no ha funcionat. - El vostre correu ha estat marcat com a "generat automàticament", la qual cosa vol dir que va ser creat automàticament per un ordinador en lloc web de ser escrit per un humà. No podem acceptar aquesta mena de missatges de correu. Si creieu que és un error, [contacteu amb un membre de l'equip responsable](%{base_url}/about). + El vostre correu ha estat marcat com a "generat automàticament", la qual cosa vol dir que ha estat creat automàticament per un ordinador en lloc de ser escrit per un humà. No podem acceptar aquesta mena de missatges de correu. Si creieu que és un error, [contacteu amb un membre de l'equip responsable](%{base_url}/about). email_reject_unrecognized_error: title: "Correu rebutjat: error no reconegut" subject_template: "[%{email_prefix}] Problema de correu electrònic: error no reconegut" @@ -2830,7 +2831,7 @@ ca: Edita la primera publicació en aquest tema per a canviar els continguts de la pàgina %{page_name}. guidelines_topic: title: "Guies/PMF" - body: " \n\n## [Aquest és un lloc web civilitzat per a la discussió pública](#civilized)\n\nTracteu aquest fòrum de discussió amb el mateix respecte amb què tractaríeu un parc públic. Nosaltres també som un recurs comunitari compartit: un lloc web per a compartir habilitats, coneixements i interessos mitjançant una conversa permanent. \n\nAquestes normes no són rígides i estrictes, sinó pautes per a ajudar el judici humà de la nostra comunitat i mantenir aquest lloc web net i endreçat per al debat públic civilitzat. \n\n\n\n## [Millorem la discussió](#improve)\n\nAjudeu-nos a fer d'aquest lloc web un lloc web ideal per a la discussió treballant sempre per millorar la discussió d'alguna manera, encara que sigui poca cosa. Si no esteu segur que la vostra publicació contribueix a la conversa d'alguna manera, penseu en el que voleu dir i torneu-ho a provar més tard.\n\nEls temes tractats aquí ens importen, i volem que actueu com si també us importessin. Sigueu respectuós amb els temes i les persones que els discuteixen, fins i tot si no esteu d'acord amb alguna cosa del que es diu.\n\nUna manera de millorar la discussió és descobrir les que ja estan en marxa. Dediqueu algun temps a navegar pels temes abans de respondre o de començar el vostre propi, i tindreu més possibilitats de conèixer altres persones que comparteixen els vostres interessos. \n\n\n\n## [Sigueu amable, fins i tot quan no esteu d'acord](#agreeable)\n\nPotser voleu respondre a alguna cosa mostrant-vos en desacord. Això està bé. Però no oblideu _criticar idees, no persones_. Eviteu:\n\n* els insults\n* els atacs _ad hominem_\n* respondre al to d'una publicació en comptes del seu contingut\n* la rèplica instintiva, reflexa\n\nEn lloc d'això, proporcioneu contraarguments raonats que millorin la conversa.\n\n\n\n## [Els vostres comptes de participació](#participate)\n\nLes converses que tenim aquí estableixen el to per a cada nouvingut. Ajudeu-nos a influir en el futur d'aquesta comunitat triant converses que facin d'aquest fòrum un lloc web interessant, i evitant les que no ho fan.\n\nDiscourse proporciona eines que permeten a la comunitat identificar col·lectivament les millors (i les pitjors) contribucions: adreces d'interès, gustos, banderes, respostes, edicions, etc. Utilitzeu aquestes eines per a millorar la vostra pròpia experiència i la de tothom.\n\nDeixem la comunitat millor que com l'hem trobada.\n\n\n\n## [Si veieu un problema, marqueu-ho amb una bandera](#flag-problems)\n\nEls moderadors tenen una autoritat especial; són els responsables del fòrum. Però vós també. Amb la vostra ajuda, els moderadors poden ser facilitadors de la comunitat, no sols els vigilants o els policies. \n\nQuan vegeu un mal comportament, no respongueu. Això fomenta el mal comportament reconeixent-lo, consumeix la vostra energia i fa perdre el temps de tothom. _Simplement marca-ho amb una bandera_. Si s'acumulen prou banderes, es durà a terme una acció, de manera automàtica o bé amb la intervenció del moderador. \n\nPer a mantenir la comunitat, els moderadors es reserven el dret d'eliminar qualsevol contingut i qualsevol compte d'usuari per qualsevol motiu en qualsevol moment. Els moderadors no previsualitzen les publicacions noves; els moderadors i els operadors del lloc web no es responsabilitzen dels continguts publicats per la comunitat. \n\n\n\n## [Sigeu sempre educat](#be-civil)\n\nNo hi ha res que espatlli una conversa sana com la grolleria: \n* Sigueu educat. No publiqueu res que una persona raonable consideri un discurs ofensiu, abusiu o d'odi.\n* Manteniu-ho net. No publiqueu res obscè o sexualment explícit.\n* Respecteu-vos mútuament. No assetgeu ni ofengueu a ningú, no suplanteu cap persona ni exposeu la seva informació privada. \n* Respecteu el nostre fòrum. No publiqueu correu brossa ni feu actes vandàlics en el fòrum. \n\nNo són termes concrets amb definicions precises: eviteu la mera _aparença_ de qualsevol d'aquestes coses. Si no esteu segur, pregunteu-vos com us sentiríeu si la vostra publicació aparegués a la primera pàgina del New York Times. \n\nAquest és un fòrum públic i els motors de cerca indexen aquestes discussions. Manteniu el llenguatge, els enllaços i les imatges segurs per a familiars i amics. \n\n\n\n## [Manteniu les coses endreçades](#keep-tidy)\n\nFeu l'esforç de posar les coses al lloc web adequat, de manera que puguem dedicar més temps a parlar i menys a netejar. Així: \n* No inicieu un tema en la categoria incorrecta. \n* No publiqueu el mateix de manera encreuada en diversos temes. \n* No publiqueu respostes sense contingut. \n* No desvieu un tema canviant-lo a mitjan conversa. \n* No signeu les vostres publicacions: cada entrada té la vostra informació de perfil adjunta. \n\nEn lloc web de publicar \"+1\" o \"D'acord\", utilitzeu el botó 'M'agrada'. En lloc web de portar un tema existent en una direcció radicalment diferent, utilitzeu 'Respon com a tema enllaçat'. \n\n\n\n## [Publiqueu sols les vostres coses](#stealing)\n\nNo podeu publicar res digital que pertanyi a algú sense permís. No podeu publicar descripcions, enllaços o mètodes per a robar la propietat intel·lectual d'algú (programari, vídeo, àudio, imatges) o per a violar qualsevol altra llei. \n\n\n\n## [Amb el vostre suport](#power)\n\nAquest lloc web és operat per l'[equip responsable](%{base_path}/about) i la comunitat. Si teniu més preguntes sobre com funcionen les coses aquí, obriu un tema nou a la [secció de comentaris sobre el lloc web](%{base_path}/c/site-feedback) i en parlem! Si hi ha un problema crític o urgent que no pot ser manejat per un metatema o una bandera, poseu-vos en contacte amb nosaltres en la [pàgina de l'equip responsable](%{base_path}/about). \n\n\n\n## [Condicions del servei](#tos) \n\nSí, el burocratès és avorrit, però hem de protegir-nos a nosaltres —i per extensió, a vosaltres i les vostres dades— contra gent poc amigable. Tenim unes [condicions del servei](%{base_path}/tos) que descriuen el vostre (i el nostre) comportament i els drets relacionats amb el contingut, la privacitat i les lleis. Per a utilitzar aquest servei, heu d'acceptar les nostres [condicions del servei](%{base_path}/tos).\n" + body: " \n\n## [Aquest és un lloc web civilitzat per a la discussió pública](#civilized)\n\nTracteu aquest fòrum de discussió amb el mateix respecte amb què tractaríeu un parc públic. Nosaltres també som un recurs comunitari compartit: un lloc web per a compartir habilitats, coneixements i interessos mitjançant una conversa permanent. \n\nAquestes normes no són rígides i estrictes, sinó pautes per a ajudar el judici humà de la nostra comunitat i mantenir aquest lloc web net i endreçat per al debat públic civilitzat. \n\n\n\n## [Millorem la discussió](#improve)\n\nAjudeu-nos a fer d'aquest lloc web un lloc web ideal per a la discussió treballant sempre per millorar la discussió d'alguna manera, encara que sigui poca cosa. Si no esteu segur que la vostra publicació contribueix a la conversa d'alguna manera, penseu en el que voleu dir i torneu-ho a provar més tard.\n\nEls temes tractats aquí ens importen, i volem que actueu com si també us importessin. Sigueu respectuós amb els temes i les persones que els discuteixen, fins i tot si no esteu d'acord amb alguna cosa del que es diu.\n\nUna manera de millorar la discussió és descobrir les que ja estan en marxa. Dediqueu algun temps a navegar pels temes abans de respondre o de començar el vostre propi, i tindreu més possibilitats de conèixer altres persones que comparteixen els vostres interessos. \n\n\n\n## [Sigueu amable, fins i tot quan no esteu d'acord](#agreeable)\n\nPotser voleu respondre a alguna cosa mostrant-vos en desacord. Això està bé. Però no oblideu _criticar idees, no persones_. Eviteu:\n\n* els insults\n* els atacs _ad hominem_\n* respondre al to d'una publicació en comptes de respondre al seu contingut\n* la rèplica instintiva, reflexa\n\nEn lloc d'això, proporcioneu contraarguments raonats que millorin la conversa.\n\n\n\n## [Els vostres comptes de participació](#participate)\n\nLes converses que tenim aquí estableixen el to per a cada nouvingut. Ajudeu-nos a influir en el futur d'aquesta comunitat triant converses que facin d'aquest fòrum un lloc web interessant, i evitant les que no ho fan.\n\nDiscourse proporciona eines que permeten a la comunitat identificar col·lectivament les millors (i les pitjors) contribucions: adreces d'interès, gustos, banderes, respostes, edicions, etc. Utilitzeu aquestes eines per a millorar la vostra pròpia experiència i la de tothom.\n\nDeixem la comunitat millor que com l'hem trobada.\n\n\n\n## [Si veieu un problema, marqueu-ho amb una bandera](#flag-problems)\n\nEls moderadors tenen una autoritat especial; són els responsables del fòrum. Però vós també. Amb la vostra ajuda, els moderadors poden ser facilitadors de la comunitat, no sols els vigilants o els policies. \n\nQuan vegeu un mal comportament, no respongueu. Això fomenta el mal comportament reconeixent-lo, consumeix la vostra energia i fa perdre el temps de tothom. _Simplement marca-ho amb una bandera_. Si s'acumulen prou banderes, es durà a terme una acció, de manera automàtica o bé amb la intervenció del moderador. \n\nPer a mantenir la comunitat, els moderadors es reserven el dret d'eliminar qualsevol contingut i qualsevol compte d'usuari per qualsevol motiu en qualsevol moment. Els moderadors no previsualitzen les publicacions noves; els moderadors i els operadors del lloc web no es responsabilitzen dels continguts publicats per la comunitat. \n\n\n\n## [Sigeu sempre educat](#be-civil)\n\nNo hi ha res que espatlli una conversa sana com la grolleria: \n* Sigueu educat. No publiqueu res que una persona raonable consideri un discurs ofensiu, abusiu o d'odi.\n* Manteniu-ho net. No publiqueu res obscè o sexualment explícit.\n* Respecteu-vos mútuament. No assetgeu ni ofengueu a ningú, no suplanteu cap persona ni exposeu la seva informació privada. \n* Respecteu el nostre fòrum. No publiqueu correu brossa ni feu actes vandàlics en el fòrum. \n\nNo són termes concrets amb definicions precises: eviteu la mera _aparença_ de qualsevol d'aquestes coses. Si no esteu segur, pregunteu-vos com us sentiríeu si la vostra publicació aparegués a la primera pàgina del New York Times. \n\nAquest és un fòrum públic i els motors de cerca indexen aquestes discussions. Manteniu el llenguatge, els enllaços i les imatges segurs per a familiars i amics. \n\n\n\n## [Manteniu les coses endreçades](#keep-tidy)\n\nFeu l'esforç de posar les coses al lloc web adequat, de manera que puguem dedicar més temps a parlar i menys a netejar. Així: \n* No inicieu un tema en la categoria incorrecta. \n* No publiqueu el mateix de manera encreuada en diversos temes. \n* No publiqueu respostes sense contingut. \n* No desvieu un tema canviant-lo a mitjan conversa. \n* No signeu les vostres publicacions: cada entrada té la vostra informació de perfil adjunta. \n\nEn comptes de publicar \"+1\" o \"D'acord\", utilitzeu el botó 'M'agrada'. En comptes de portar un tema existent en una direcció radicalment diferent, utilitzeu 'Respon com a tema enllaçat'. \n\n\n\n## [Publiqueu sols les vostres coses](#stealing)\n\nNo podeu publicar res digital que pertanyi a algú sense permís. No podeu publicar descripcions, enllaços o mètodes per a robar la propietat intel·lectual d'algú (programari, vídeo, àudio, imatges) o per a violar qualsevol altra llei. \n\n\n\n## [Amb el vostre suport](#power)\n\nAquest lloc web és operat per l'[equip responsable](%{base_path}/about) i la comunitat. Si teniu més preguntes sobre com funcionen les coses aquí, obriu un tema nou a la [secció de comentaris sobre el lloc web](%{base_path}/c/site-feedback) i en parlem! Si hi ha un problema crític o urgent que no pot ser manejat per un metatema o una bandera, poseu-vos en contacte amb nosaltres en la [pàgina de l'equip responsable](%{base_path}/about). \n\n\n\n## [Condicions del servei](#tos) \n\nSí, el burocratès és avorrit, però hem de protegir-nos a nosaltres —i per extensió, a vosaltres i les vostres dades— contra gent poc amigable. Tenim unes [condicions del servei](%{base_path}/tos) que descriuen el vostre (i el nostre) comportament i els drets relacionats amb el contingut, la privacitat i les lleis. Per a utilitzar aquest servei, heu d'acceptar les nostres [condicions del servei](%{base_path}/tos).\n" tos_topic: title: "Condicions del servei" body: "Aquestes condicions regeixen l'ús del fòrum d'Internet en <%{base_url}>. Per a utilitzar el fòrum, heu d'acceptar aquests termes amb %{company_name}, la companyia que porta el fòrum. \n\nL'empresa pot oferir altres productes i serveis sota diferents condicions. Aquestes condicions solament s'apliquen a l'ús del fòrum. \n\nSalteu a: \n- [Condicions importants](#heading--permission)\n- [El vostre permís per a utilitzar el fòrum](#heading--permission) \n- [Condicions d'ús del fòrum](#heading--conditions) \n- [Ús acceptable](#heading--acceptable-use) \n- [Normes de contingut](#heading--content-standards) \n- [Aplicació](#heading-enforcement) \n- [El vostre compte](#heading--your-account) \n- [El vostre contingut](#heading--your-account) \n- [La vostra responsabilitat](#heading--your-responsibility) \n- [Exempció de responsabilitat](#heading--disclaimers) \n- [Límits de responsabilitat](#heading--liability) \n- [Comentaris](#heading--feedback)\n- [Terminació](#heading--termination) \n- [Disputes](#heading--disputes) \n- [Condicions generals](#heading--general) \n- [Contacte](#heading-contact) \n- [Canvis](#heading--changes) \n\n

Condicions importants

\n\n***Aquestes condicions inclouen una sèrie de disposicions importants que afecten els vostres drets i responsabilitats, com ara les renúncies a [exempcions de responsabilitat](#heading--disclaimers), límits en la responsabilitat de l'empresa respecte a vós en [Límit de responsabilitat](#heading--liability), el vostre consentiment a cobrir l'empresa per danys causats pel vostre ús indegut del fòrum en [La vostra responsabilitat](#heading--responsibility) i un acord d'arbitratge de controvèrsies en [Disputes](#header--disputes).***\n\n

El vostre permís per a utilitzar el fòrum

\n\nSegons aquestes condicions, l'empresa us dóna permís per a utilitzar el fòrum. Tothom ha d'acceptar aquestes condicions per a fer servir el fòrum. \n\n

Condicions d'ús del fòrum

\n\nEl permís que se us dóna per a utilitzar el fòrum està subjecte a les condicions següents: \n\n1. Heu de tenir almenys tretze anys. \n\n2. No podreu fer servir més el fòrum si l'empresa es posa en contacte directament amb vós per a dir-vos que no podeu. \n\n3. Heu d'utilitzar el fòrum d'acord amb l'[Ús acceptable](#heading--acceptable-use) i les [Normes de contingut](#heading--content-standards).\n\n

Ús acceptable

\n\n1. No heu d'infringir la llei fent servir el fòrum. \n\n2. No podeu utilitzar o intentar utilitzar el compte d'altres persones en el fòrum sense el seu permís específic. \n\n3. No podeu comprar, vendre o comerciar en noms d'usuari o altres identificadors únics en el fòrum. \n\n4. No podeu enviar anuncis, cartes en cadena ni altres sol·licituds per mitjà del fòrum ni utilitzar el fòrum per a recopilar adreces o altres dades personals per a llistes de correu comercials o bases de dades. \n\n5. No podeu automatitzar l'accés al fòrum ni monitorar el fòrum, com ara amb un rastrejador web, un complement o connector del navegador, o un altre programa d'ordinador que no sigui un navegador web. Podeu rastrejar el fòrum per a indexar-lo per a un motor de cerca disponible públicament, si en gestioneu un. \n\n6. No podeu fer servir el fòrum per a enviar correu electrònic a llistes de distribució, grups de notícies o àlies de correu de grup. \n\n7. No podeu induir a pensar falsament que esteu afiliats amb la companyia o que teniu el seu suport. \n\n8. No podeu enllaçar a imatges o altres continguts que no siguin hipertext del fòrum en altres pàgines web. \n\n9. No podeu suprimir cap marca que mostri la propietat propietària dels materials que baixeu del fòrum. \n\n10. No podeu mostrar cap part del fòrum a altres llocs web amb `
{{text-field value=loginName placeholderKey="login.email_placeholder" id="login-account-name" autocorrect="off" autocapitalize="off" autofocus="autofocus" disabled=showSecondFactor}}{{text-field value=loginName placeholderKey="login.email_placeholder" id="login-account-name" autocorrect="off" autocapitalize="off" autofocus="autofocus" disabled=disableLoginFields}}
{{password-field value=loginPassword type="password" id="login-account-password" maxlength="200" capsLockOn=capsLockOn disabled=showSecondFactor}}{{password-field value=loginPassword type="password" id="login-account-password" maxlength="200" capsLockOn=capsLockOn disabled=disableLoginFields}} {{i18n 'forgot_password.action'}}