diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a0f06bf462..1ab70c8c80 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,7 +10,7 @@ updates: interval: daily time: "08:00" timezone: Australia/Sydney - open-pull-requests-limit: 10 + open-pull-requests-limit: 20 versioning-strategy: lockfile-only allow: - dependency-type: direct diff --git a/Gemfile b/Gemfile index daf68be0cb..3bbbfee3c0 100644 --- a/Gemfile +++ b/Gemfile @@ -63,15 +63,8 @@ gem 'active_model_serializers', '~> 0.8.3' gem 'http_accept_language', require: false -# Ember related gems need to be pinned cause they control client side -# behavior, we will push these versions up when upgrading ember -gem 'discourse-ember-rails', '0.18.6', require: 'ember-rails' -gem 'discourse-ember-source', '~> 3.12.2' -gem 'ember-handlebars-template', '0.8.0' gem 'discourse-fonts', require: 'discourse_fonts' -gem 'barber' - gem 'message_bus' gem 'rails_multisite' diff --git a/Gemfile.lock b/Gemfile.lock index 29506c76cb..468062cd2e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -73,16 +73,13 @@ GEM aws-sigv4 (~> 1.1) aws-sigv4 (1.5.0) aws-eventstream (~> 1, >= 1.0.2) - barber (0.12.2) - ember-source (>= 1.0, < 3.1) - execjs (>= 1.2, < 3) better_errors (2.9.1) coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) - bootsnap (1.13.0) + bootsnap (1.15.0) msgpack (~> 1.2) builder (3.2.4) bullet (7.0.3) @@ -119,14 +116,6 @@ GEM diff-lcs (1.5.0) diffy (3.4.2) digest (3.1.0) - discourse-ember-rails (0.18.6) - active_model_serializers - ember-data-source (>= 1.0.0.beta.5) - ember-handlebars-template (>= 0.1.1, < 1.0) - ember-source (>= 1.1.0) - jquery-rails (>= 1.0.17) - railties (>= 3.1) - discourse-ember-source (3.12.2.3) discourse-fonts (0.0.9) discourse-seed-fu (2.3.12) activerecord (>= 3.1) @@ -138,12 +127,6 @@ GEM ecma-re-validator (0.4.0) regexp_parser (~> 2.2) email_reply_trimmer (0.1.13) - ember-data-source (3.0.2) - ember-source (>= 2, < 3.0) - ember-handlebars-template (0.8.0) - barber (>= 0.11.0) - sprockets (>= 3.3, < 4.1) - ember-source (2.18.2) erubi (1.11.0) excon (0.94.0) execjs (2.8.1) @@ -152,10 +135,10 @@ GEM faker (2.23.0) i18n (>= 1.8.11, < 2) fakeweb (1.3.0) - faraday (2.6.0) + faraday (2.7.1) faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.1) + faraday-net_http (3.0.2) faraday-retry (2.0.0) faraday (~> 2.0) fast_blank (1.0.1) @@ -176,7 +159,7 @@ GEM http_accept_language (2.1.1) i18n (1.12.0) concurrent-ruby (~> 1.0) - image_optim (0.31.1) + image_optim (0.31.2) exifr (~> 1.2, >= 1.2.2) fspath (~> 3.0) image_size (>= 1.5, < 4) @@ -184,11 +167,7 @@ GEM progress (~> 3.0, >= 3.0.1) image_size (3.2.0) in_threads (1.6.0) - jmespath (1.6.1) - jquery-rails (4.5.1) - rails-dom-testing (>= 1, < 3) - railties (>= 4.2.0) - thor (>= 0.14, < 2.0) + jmespath (1.6.2) json (2.6.2) json-schema (3.0.0) addressable (>= 2.8) @@ -226,15 +205,15 @@ GEM matrix (0.4.2) maxminddb (0.1.22) memory_profiler (1.0.1) - message_bus (4.2.0) + message_bus (4.3.0) rack (>= 1.1.3) method_source (1.0.0) mini_mime (1.1.2) mini_portile2 (2.8.0) mini_racer (0.6.3) libv8-node (~> 16.10.0.0) - mini_scheduler (0.14.0) - sidekiq (>= 4.2.3) + mini_scheduler (0.15.0) + sidekiq (>= 4.2.3, < 7.0) mini_sql (1.4.0) mini_suffix (0.3.3) ffi (~> 1.9) @@ -309,9 +288,9 @@ GEM parallel (1.22.1) parallel_tests (4.0.0) parallel - parser (3.1.2.1) + parser (3.1.3.0) ast (~> 2.4.1) - pg (1.4.4) + pg (1.4.5) progress (3.6.0) pry (0.14.1) coderay (~> 1.1) @@ -322,7 +301,7 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (5.0.0) - puma (5.6.5) + puma (6.0.0) nio4r (~> 2.0) r2 (0.2.7) racc (1.6.0) @@ -366,7 +345,7 @@ GEM redis (4.7.1) redis-namespace (1.9.0) redis (>= 4) - regexp_parser (2.6.0) + regexp_parser (2.6.1) request_store (1.5.1) rack (>= 1.4) rexml (3.2.5) @@ -402,12 +381,12 @@ GEM rspec-support (3.12.0) rss (0.2.9) rexml - rswag-specs (2.7.0) + rswag-specs (2.8.0) activesupport (>= 3.1, < 7.1) json-schema (>= 2.2, < 4.0) railties (>= 3.1, < 7.1) rspec-core (>= 2.14) - rubocop (1.38.0) + rubocop (1.39.0) json (~> 2.3) parallel (~> 1.10) parser (>= 3.1.2.1) @@ -450,8 +429,8 @@ GEM websocket (~> 1.0) shoulda-matchers (5.2.0) activesupport (>= 5.2.0) - sidekiq (6.5.7) - connection_pool (>= 2.2.5) + sidekiq (6.5.8) + connection_pool (>= 2.2.5, < 3) rack (~> 2.0) redis (>= 4.5.0, < 5) simplecov (0.21.2) @@ -531,7 +510,6 @@ DEPENDENCIES annotate aws-sdk-s3 aws-sdk-sns - barber better_errors binding_of_caller bootsnap @@ -546,13 +524,10 @@ DEPENDENCIES css_parser diffy digest - discourse-ember-rails (= 0.18.6) - discourse-ember-source (~> 3.12.2) discourse-fonts discourse-seed-fu discourse_dev_assets email_reply_trimmer - ember-handlebars-template (= 0.8.0) excon execjs fabrication diff --git a/app/assets/javascripts/admin/addon/templates/dashboard_moderation.hbs b/app/assets/javascripts/admin/addon/templates/dashboard_moderation.hbs index 923b50d1e3..7c4537afbc 100644 --- a/app/assets/javascripts/admin/addon/templates/dashboard_moderation.hbs +++ b/app/assets/javascripts/admin/addon/templates/dashboard_moderation.hbs @@ -25,6 +25,11 @@ - + diff --git a/app/assets/javascripts/admin/addon/templates/user-index.hbs b/app/assets/javascripts/admin/addon/templates/user-index.hbs index 8bf8131f57..9e1e11b79b 100644 --- a/app/assets/javascripts/admin/addon/templates/user-index.hbs +++ b/app/assets/javascripts/admin/addon/templates/user-index.hbs @@ -38,7 +38,7 @@ {{#if this.model.email}} {{this.model.email}} {{else}} - + {{/if}}
@@ -65,7 +65,7 @@ {{i18n "user.email.no_secondary"}} {{/if}} {{else}} - + {{/if}}
@@ -99,7 +99,7 @@ {{#if this.associatedAccountsLoaded}} {{this.associatedAccounts}} {{else}} - + {{/if}} @@ -561,7 +561,7 @@ {{#if this.ssoExternalEmail}}
{{this.ssoExternalEmail}}
{{else}} - + {{/if}} {{/if}} diff --git a/app/assets/javascripts/admin/jsconfig.json b/app/assets/javascripts/admin/jsconfig.json index 43b162f6ac..990839f3fe 100644 --- a/app/assets/javascripts/admin/jsconfig.json +++ b/app/assets/javascripts/admin/jsconfig.json @@ -5,6 +5,7 @@ "paths": { "admin/*": ["./addon/*"], "discourse/*": ["../discourse/app/*"], + "discourse/tests/*": ["../discourse/tests/*"], "discourse-common/*": ["../discourse-common/addon/*"], "pretty-text/*": ["../pretty-text/addon/*"], } diff --git a/app/assets/javascripts/admin/package.json b/app/assets/javascripts/admin/package.json index de2970fa2f..ec6e3d0136 100644 --- a/app/assets/javascripts/admin/package.json +++ b/app/assets/javascripts/admin/package.json @@ -18,12 +18,13 @@ "ember-auto-import": "^2.4.3", "ember-cli-babel": "^7.26.10", "ember-cli-htmlbars": "^6.1.1", - "webpack": "^5.74.0", + "webpack": "^5.75.0", "xss": "^1.0.14" }, "devDependencies": { + "@babel/core": "^7.20.2", "@ember/optional-features": "^2.0.0", - "@embroider/test-setup": "^1.8.3", + "@embroider/test-setup": "^2.0.0", "@glimmer/component": "^1.1.2", "broccoli-asset-rev": "^3.0.0", "ember-cli": "~3.28.5", diff --git a/app/assets/javascripts/discourse/lib/bootstrap-json/.npmrc b/app/assets/javascripts/bootstrap-json/.npmrc similarity index 100% rename from app/assets/javascripts/discourse/lib/bootstrap-json/.npmrc rename to app/assets/javascripts/bootstrap-json/.npmrc diff --git a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js b/app/assets/javascripts/bootstrap-json/index.js similarity index 98% rename from app/assets/javascripts/discourse/lib/bootstrap-json/index.js rename to app/assets/javascripts/bootstrap-json/index.js index 1c778d80a0..5b708c1c96 100644 --- a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js +++ b/app/assets/javascripts/bootstrap-json/index.js @@ -1,14 +1,13 @@ "use strict"; const express = require("express"); -const fetch = require("node-fetch"); const { encode } = require("html-entities"); const cleanBaseURL = require("clean-base-url"); const path = require("path"); const fs = require("fs"); const fsPromises = fs.promises; const { JSDOM } = require("jsdom"); -const { shouldLoadPluginTestJs } = require("discourse/lib/plugin-js"); +const { shouldLoadPluginTestJs } = require("discourse-plugins"); const { Buffer } = require("node:buffer"); const { cwd, env } = require("node:process"); @@ -258,6 +257,7 @@ async function buildFromBootstrap(proxy, baseURL, req, response, preload) { url.searchParams.append("preview_theme_id", reqUrlPreviewThemeId); } + const { default: fetch } = await import("node-fetch"); const res = await fetch(url, { headers: req.headers }); const json = await res.json(); @@ -310,6 +310,7 @@ async function handleRequest(proxy, baseURL, req, res) { req.headers["X-Discourse-Ember-CLI"] = "true"; } + const { default: fetch } = await import("node-fetch"); const response = await fetch(url, { method: req.method, body: /GET|HEAD/.test(req.method) ? null : req.body, diff --git a/app/assets/javascripts/bootstrap-json/package.json b/app/assets/javascripts/bootstrap-json/package.json new file mode 100644 index 0000000000..ce989fc828 --- /dev/null +++ b/app/assets/javascripts/bootstrap-json/package.json @@ -0,0 +1,30 @@ +{ + "name": "bootstrap-json", + "version": "1.0.0", + "description": "Express.js middleware that proxies ember cli requests and fetches bootstrap json", + "author": "Discourse", + "license": "GPL-2.0-only", + "keywords": [ + "ember-addon" + ], + "ember-addon": { + "before": [ + "serve-files-middleware", + "history-support-middleware", + "proxy-server-middleware" + ] + }, + "engines": { + "node": "16.* || >= 18", + "npm": "please-use-yarn", + "yarn": ">= 1.21.1" + }, + "dependencies": { + "clean-base-url": "^1.0.0", + "discourse-plugins": "1.0.0", + "express": "^4.18.2", + "html-entities": "^2.3.3", + "jsdom": "^20.0.3", + "node-fetch": "^3.3.0" + } +} diff --git a/app/assets/javascripts/discourse/lib/dialog-holder/addon/components/dialog-holder.hbs b/app/assets/javascripts/dialog-holder/addon/components/dialog-holder.hbs similarity index 100% rename from app/assets/javascripts/discourse/lib/dialog-holder/addon/components/dialog-holder.hbs rename to app/assets/javascripts/dialog-holder/addon/components/dialog-holder.hbs diff --git a/app/assets/javascripts/discourse/lib/dialog-holder/addon/components/dialog-holder.js b/app/assets/javascripts/dialog-holder/addon/components/dialog-holder.js similarity index 100% rename from app/assets/javascripts/discourse/lib/dialog-holder/addon/components/dialog-holder.js rename to app/assets/javascripts/dialog-holder/addon/components/dialog-holder.js diff --git a/app/assets/javascripts/discourse/lib/dialog-holder/addon/services/dialog.js b/app/assets/javascripts/dialog-holder/addon/services/dialog.js similarity index 100% rename from app/assets/javascripts/discourse/lib/dialog-holder/addon/services/dialog.js rename to app/assets/javascripts/dialog-holder/addon/services/dialog.js diff --git a/app/assets/javascripts/discourse/lib/dialog-holder/app/components/dialog-holder.js b/app/assets/javascripts/dialog-holder/app/components/dialog-holder.js similarity index 100% rename from app/assets/javascripts/discourse/lib/dialog-holder/app/components/dialog-holder.js rename to app/assets/javascripts/dialog-holder/app/components/dialog-holder.js diff --git a/app/assets/javascripts/discourse/lib/dialog-holder/app/services/dialog.js b/app/assets/javascripts/dialog-holder/app/services/dialog.js similarity index 100% rename from app/assets/javascripts/discourse/lib/dialog-holder/app/services/dialog.js rename to app/assets/javascripts/dialog-holder/app/services/dialog.js diff --git a/app/assets/javascripts/discourse/lib/dialog-holder/index.js b/app/assets/javascripts/dialog-holder/index.js similarity index 100% rename from app/assets/javascripts/discourse/lib/dialog-holder/index.js rename to app/assets/javascripts/dialog-holder/index.js diff --git a/app/assets/javascripts/dialog-holder/package.json b/app/assets/javascripts/dialog-holder/package.json new file mode 100644 index 0000000000..d441078497 --- /dev/null +++ b/app/assets/javascripts/dialog-holder/package.json @@ -0,0 +1,24 @@ +{ + "name": "dialog-holder", + "version": "1.0.0", + "description": "TODO", + "author": "Discourse", + "license": "GPL-2.0-only", + "keywords": [ + "ember-addon" + ], + "dependencies": { + "a11y-dialog": "7.5.2", + "ember-auto-import": "^2.4.3", + "ember-cli-babel": "^7.26.10", + "ember-cli-htmlbars": "^6.1.1" + }, + "devDependencies": { + "webpack": "^5.75.0" + }, + "engines": { + "node": "16.* || >= 18", + "npm": "please-use-yarn", + "yarn": ">= 1.21.1" + } +} diff --git a/app/assets/javascripts/discourse/lib/dialog-holder/yarn.lock b/app/assets/javascripts/dialog-holder/yarn.lock similarity index 100% rename from app/assets/javascripts/discourse/lib/dialog-holder/yarn.lock rename to app/assets/javascripts/dialog-holder/yarn.lock diff --git a/app/assets/javascripts/discourse-common/addon/config/environment.js b/app/assets/javascripts/discourse-common/addon/config/environment.js index f4dc97ced1..5208c1f756 100644 --- a/app/assets/javascripts/discourse-common/addon/config/environment.js +++ b/app/assets/javascripts/discourse-common/addon/config/environment.js @@ -19,6 +19,7 @@ export function isTesting() { // Generally means "before we migrated to Ember CLI" export function isLegacyEmber() { deprecated("`isLegacyEmber()` is now deprecated and always returns false", { + id: "discourse.is-legacy-ember", dropFrom: "3.0.0.beta1", }); return false; diff --git a/app/assets/javascripts/discourse-common/addon/lib/deprecated.js b/app/assets/javascripts/discourse-common/addon/lib/deprecated.js index 1c36871d6a..4752732e3c 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/deprecated.js +++ b/app/assets/javascripts/discourse-common/addon/lib/deprecated.js @@ -1,17 +1,39 @@ -export default function deprecated(msg, opts = {}) { - msg = ["Deprecation notice:", msg]; - if (opts.since) { - msg.push(`(deprecated since Discourse ${opts.since})`); +const handlers = []; +const disabledDeprecations = new Set(); + +/** + * Display a deprecation warning with the provided message. The warning will be prefixed with the theme/plugin name + * if it can be automatically determined based on the current stack. + * @param {String} msg The deprecation message + * @param {Object} [options] Deprecation options + * @param {String} [options.id] A unique identifier for this deprecation. This should be namespaced by dots (e.g. discourse.my_deprecation) + * @param {String} [options.since] The Discourse version this deprecation was introduced in + * @param {String} [options.dropFrom] The Discourse version this deprecation will be dropped in. Typically one major version after `since` + * @param {String} [options.url] A URL which provides more detail about the deprecation + * @param {boolean} [options.raiseError] Raise an error when this deprecation is triggered. Defaults to `false` + */ +export default function deprecated(msg, options = {}) { + const { id, since, dropFrom, url, raiseError } = options; + + if (id && disabledDeprecations.has(id)) { + return; } - if (opts.dropFrom) { - msg.push(`(removal in Discourse ${opts.dropFrom})`); + + msg = ["Deprecation notice:", msg]; + if (since) { + msg.push(`[deprecated since Discourse ${since}]`); + } + if (dropFrom) { + msg.push(`[removal in Discourse ${dropFrom}]`); + } + if (id) { + msg.push(`[deprecation id: ${id}]`); + } + if (url) { + msg.push(`[info: ${url}]`); } msg = msg.join(" "); - if (opts.raiseError) { - throw msg; - } - let consolePrefix = ""; if (window.Discourse) { // This module doesn't exist in pretty-text/wizard/etc. @@ -19,5 +41,56 @@ export default function deprecated(msg, opts = {}) { require("discourse/lib/source-identifier").consolePrefix() || ""; } - console.warn(consolePrefix, msg); //eslint-disable-line no-console + handlers.forEach((h) => h(msg, options)); + + if (raiseError) { + throw msg; + } + + console.warn(...[consolePrefix, msg].filter(Boolean)); //eslint-disable-line no-console +} + +/** + * Register a function which will be called whenever a deprecation is triggered + * @param {function} callback The callback function. Arguments will match those of `deprecated()`. + */ +export function registerDeprecationHandler(callback) { + handlers.push(callback); +} + +/** + * Silence one or more deprecations while running `callback` + * @param {(string|string[])} deprecationIds A single id, or an array of ids, of deprecations to silence + * @param {function} callback The function to call while deprecations are silenced. + */ +export function withSilencedDeprecations(deprecationIds, callback) { + const idArray = [].concat(deprecationIds); + try { + idArray.forEach((id) => disabledDeprecations.add(id)); + const result = callback(); + if (result instanceof Promise) { + throw new Error( + "withSilencedDeprecations callback returned a promise. Use withSilencedDeprecationsAsync instead." + ); + } + return result; + } finally { + idArray.forEach((id) => disabledDeprecations.delete(id)); + } +} + +/** + * Silence one or more deprecations while running an async `callback` + * @async + * @param {(string|string[])} deprecationIds A single id, or an array of ids, of deprecations to silence + * @param {function} callback The asynchronous function to call while deprecations are silenced. + */ +export async function withSilencedDeprecationsAsync(deprecationIds, callback) { + const idArray = [].concat(deprecationIds); + try { + idArray.forEach((id) => disabledDeprecations.add(id)); + return await callback(); + } finally { + idArray.forEach((id) => disabledDeprecations.delete(id)); + } } diff --git a/app/assets/javascripts/discourse-common/addon/lib/get-owner.js b/app/assets/javascripts/discourse-common/addon/lib/get-owner.js index 94a12c2358..27b31f533b 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/get-owner.js +++ b/app/assets/javascripts/discourse-common/addon/lib/get-owner.js @@ -33,7 +33,8 @@ export function getRegister(obj) { Object.defineProperty(target, "container", { get() { deprecated( - "Use `this.register` or `getOwner` instead of `this.container`" + "Use `this.register` or `getOwner` instead of `this.container`", + { id: "discourse.this-container" } ); return register; }, diff --git a/app/assets/javascripts/discourse-common/addon/lib/icon-library.js b/app/assets/javascripts/discourse-common/addon/lib/icon-library.js index a969a2a116..df1149d8a5 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/icon-library.js +++ b/app/assets/javascripts/discourse-common/addon/lib/icon-library.js @@ -34,8 +34,8 @@ const REPLACEMENTS = { "notification.liked_2": "heart", "notification.liked_many": "heart", "notification.liked_consolidated": "heart", - "notification.private_message": "far-envelope", - "notification.invited_to_private_message": "far-envelope", + "notification.private_message": "envelope", + "notification.invited_to_private_message": "envelope", "notification.invited_to_topic": "hand-point-right", "notification.invitee_accepted": "user", "notification.moved_post": "sign-out-alt", @@ -169,6 +169,7 @@ registerIconRenderer({ deprecated(`use 'translatedTitle' option instead of 'translatedtitle'`, { since: "2.9.0.beta6", dropFrom: "2.10.0.beta1", + id: "discourse.icon-renderer-translatedtitle", }); params.translatedTitle = params.translatedtitle; } diff --git a/app/assets/javascripts/discourse-common/addon/lib/legacy-resolver.js b/app/assets/javascripts/discourse-common/addon/lib/legacy-resolver.js index 6ac223c53a..6a3ab1d6d3 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/legacy-resolver.js +++ b/app/assets/javascripts/discourse-common/addon/lib/legacy-resolver.js @@ -66,7 +66,11 @@ export function buildResolver(baseName) { if (fullName === "app-events:main") { deprecated( "`app-events:main` has been replaced with `service:app-events`", - { since: "2.4.0", dropFrom: "2.9.0.beta1" } + { + since: "2.4.0", + dropFrom: "2.9.0.beta1", + id: "discourse.app-events-main", + } ); return "service:app-events"; } @@ -84,7 +88,10 @@ export function buildResolver(baseName) { "route:tagsShow": "route:tagShow", })) { if (fullName === key) { - deprecated(`${key} was replaced with ${value}`, { since: "2.6.0" }); + deprecated(`${key} was replaced with ${value}`, { + since: "2.6.0", + id: "discourse.legacy-resolver-resolutions", + }); return value; } } diff --git a/app/assets/javascripts/discourse-common/addon/resolver.js b/app/assets/javascripts/discourse-common/addon/resolver.js index 2c18d4fb7c..079161f604 100644 --- a/app/assets/javascripts/discourse-common/addon/resolver.js +++ b/app/assets/javascripts/discourse-common/addon/resolver.js @@ -168,6 +168,7 @@ export function buildResolver(baseName) { { since: deprecationInfo.since, dropFrom: deprecationInfo.dropFrom, + id: "discourse.resolver-resolutions", } ); } @@ -264,7 +265,8 @@ export function buildResolver(baseName) { resolved = this.legacyResolver.resolveOther(legacyParsedName); if (resolved) { deprecated( - `Unable to resolve with new resolver, but resolved with legacy resolver: ${parsedName.fullName}` + `Unable to resolve with new resolver, but resolved with legacy resolver: ${parsedName.fullName}`, + { id: "discourse.legacy-resolver-fallback" } ); } } diff --git a/app/assets/javascripts/discourse-common/addon/utils/macro-alias.js b/app/assets/javascripts/discourse-common/addon/utils/macro-alias.js index bfcc2af2cc..7a2f4f8720 100644 --- a/app/assets/javascripts/discourse-common/addon/utils/macro-alias.js +++ b/app/assets/javascripts/discourse-common/addon/utils/macro-alias.js @@ -18,7 +18,8 @@ export default function macroAlias(fn) { return handleDescriptor(...params, fn); } else { deprecated( - `Importing ${fn.name} from 'discourse-common/utils/decorators' is deprecated. You should instead import it from '@ember/object/computed' directly.` + `Importing ${fn.name} from 'discourse-common/utils/decorators' is deprecated. You should instead import it from '@ember/object/computed' directly.`, + { id: "discourse.utils-decorators-import" } ); return function (target, property, desc) { return handleDescriptor(target, property, desc, fn, params); diff --git a/app/assets/javascripts/discourse-common/package.json b/app/assets/javascripts/discourse-common/package.json index 680b243211..dabe771b45 100644 --- a/app/assets/javascripts/discourse-common/package.json +++ b/app/assets/javascripts/discourse-common/package.json @@ -26,12 +26,13 @@ "ember-cli-htmlbars": "^6.1.1", "ember-resolver": "^8.0.3", "handlebars": "^4.7.0", - "truth-helpers": "^1.0.0", - "webpack": "^5.74.0" + "truth-helpers": "1.0.0", + "webpack": "^5.75.0" }, "devDependencies": { + "@babel/core": "^7.20.2", "@ember/optional-features": "^2.0.0", - "@embroider/test-setup": "^1.8.3", + "@embroider/test-setup": "^2.0.0", "@glimmer/component": "^1.1.2", "broccoli-asset-rev": "^3.0.0", "ember-cli": "~3.28.5", diff --git a/app/assets/javascripts/discourse-hbr/package.json b/app/assets/javascripts/discourse-hbr/package.json index aec662621a..311f32c005 100644 --- a/app/assets/javascripts/discourse-hbr/package.json +++ b/app/assets/javascripts/discourse-hbr/package.json @@ -19,11 +19,12 @@ "ember-cli-babel": "^7.26.10", "ember-cli-htmlbars": "^6.1.1", "handlebars": "^4.7.6", - "webpack": "^5.74.0" + "webpack": "^5.75.0" }, "devDependencies": { + "@babel/core": "^7.20.2", "@ember/optional-features": "^2.0.0", - "@embroider/test-setup": "^1.8.3", + "@embroider/test-setup": "^2.0.0", "@glimmer/component": "^1.1.2", "broccoli-asset-rev": "^3.0.0", "ember-cli": "~3.28.5", diff --git a/app/assets/javascripts/discourse-plugins/index.js b/app/assets/javascripts/discourse-plugins/index.js index 4b5ed201fb..730f926472 100644 --- a/app/assets/javascripts/discourse-plugins/index.js +++ b/app/assets/javascripts/discourse-plugins/index.js @@ -8,6 +8,7 @@ const fs = require("fs"); const concat = require("broccoli-concat"); const RawHandlebarsCompiler = require("discourse-hbr/raw-handlebars-compiler"); const DiscoursePluginColocatedTemplateProcessor = require("./colocated-template-compiler"); +const EmberApp = require("ember-cli/lib/broccoli/ember-app"); function fixLegacyExtensions(tree) { return new Funnel(tree, { @@ -35,7 +36,7 @@ function unColocateConnectors(tree) { if ( match && match.groups.extension === "hbs" && - !match.groups.prefix.endsWith("/templates") + match.groups.prefix.split("/").pop() !== "templates" ) { const { prefix, outlet, name } = match.groups; return `${prefix}/templates/connectors/${outlet}/${name}.hbs`; @@ -43,7 +44,7 @@ function unColocateConnectors(tree) { if ( match && match.groups.extension === "js" && - match.groups.prefix.endsWith("/templates") + match.groups.prefix.split("/").pop() === "templates" ) { // Some plugins are colocating connector JS under `/templates` const { prefix, outlet, name } = match.groups; @@ -55,10 +56,10 @@ function unColocateConnectors(tree) { }); } -function namespaceModules(tree, pluginDirectoryName) { +function namespaceModules(tree, pluginName) { return new Funnel(tree, { getDestinationPath: function (relativePath) { - return `discourse/plugins/${pluginDirectoryName}/${relativePath}`; + return `discourse/plugins/${pluginName}/${relativePath}`; }, }); } @@ -217,4 +218,8 @@ module.exports = { // This addon doesn't contribute any 'real' trees to the app return; }, + + shouldLoadPluginTestJs() { + return EmberApp.env() === "development" || process.env.LOAD_PLUGINS === "1"; + }, }; diff --git a/app/assets/javascripts/discourse-plugins/package.json b/app/assets/javascripts/discourse-plugins/package.json index e57cc55bcc..ca5b70a81b 100644 --- a/app/assets/javascripts/discourse-plugins/package.json +++ b/app/assets/javascripts/discourse-plugins/package.json @@ -9,10 +9,15 @@ ], "repository": "", "dependencies": { + "discourse-widget-hbs": "1.0.0", "ember-auto-import": "^2.4.3", + "ember-cli": "~3.28.5", "ember-cli-babel": "^7.26.10", "ember-cli-htmlbars": "^6.1.1", - "discourse-widget-hbs": "1.0" + "webpack": "^5.75.0" + }, + "devDependencies": { + "@babel/core": "^7.20.2" }, "engines": { "node": "16.* || >= 18", diff --git a/app/assets/javascripts/discourse-widget-hbs/jsconfig.json b/app/assets/javascripts/discourse-widget-hbs/jsconfig.json index eba0a58809..984b8c55f5 100644 --- a/app/assets/javascripts/discourse-widget-hbs/jsconfig.json +++ b/app/assets/javascripts/discourse-widget-hbs/jsconfig.json @@ -5,6 +5,7 @@ "paths": { "discourse-widget-hbs/*": ["./addon/*"], "discourse/*": ["../discourse/app/*"], + "discourse/tests/*": ["../discourse/tests/*"], "discourse-common/*": ["../discourse-common/addon/*"] } }, diff --git a/app/assets/javascripts/discourse-widget-hbs/package.json b/app/assets/javascripts/discourse-widget-hbs/package.json index ada9dcfcf5..499ac8a5c5 100644 --- a/app/assets/javascripts/discourse-widget-hbs/package.json +++ b/app/assets/javascripts/discourse-widget-hbs/package.json @@ -19,11 +19,12 @@ "ember-cli-babel": "^7.26.10", "ember-cli-htmlbars": "^6.1.1", "handlebars": "^4.7.6", - "webpack": "^5.74.0" + "webpack": "^5.75.0" }, "devDependencies": { + "@babel/core": "^7.20.2", "@ember/optional-features": "^2.0.0", - "@embroider/test-setup": "^1.8.3", + "@embroider/test-setup": "^2.0.0", "@glimmer/component": "^1.1.2", "@glimmer/syntax": "^0.84.2", "broccoli-asset-rev": "^3.0.0", diff --git a/app/assets/javascripts/discourse/app/components/bookmark-list.js b/app/assets/javascripts/discourse/app/components/bookmark-list.js index 6ea4c9cb63..15dd1f42ca 100644 --- a/app/assets/javascripts/discourse/app/components/bookmark-list.js +++ b/app/assets/javascripts/discourse/app/components/bookmark-list.js @@ -75,10 +75,9 @@ export default Component.extend(Scrolling, { @action screenExcerptForExternalLink(event) { - if (event.target && event.target.tagName === "A") { - let link = event.target; - if (shouldOpenInNewTab(link.href)) { - openLinkInNewTab(link); + if (event?.target?.tagName === "A") { + if (shouldOpenInNewTab(event.target.href)) { + openLinkInNewTab(event, event.target); } } }, diff --git a/app/assets/javascripts/discourse/app/components/bread-crumbs.js b/app/assets/javascripts/discourse/app/components/bread-crumbs.js index 4f0925706f..cff17a5cc2 100644 --- a/app/assets/javascripts/discourse/app/components/bread-crumbs.js +++ b/app/assets/javascripts/discourse/app/components/bread-crumbs.js @@ -57,14 +57,16 @@ export default Component.extend({ @discourseComputed("category") parentCategory(category) { deprecated( - "The parentCategory property of the bread-crumbs component is deprecated" + "The parentCategory property of the bread-crumbs component is deprecated", + { id: "discourse.breadcrumbs.parentCategory" } ); return category && category.parentCategory; }, parentCategories: filter("categories", function (c) { deprecated( - "The parentCategories property of the bread-crumbs component is deprecated" + "The parentCategories property of the bread-crumbs component is deprecated", + { id: "discourse.breadcrumbs.parentCategories" } ); if ( c.id === this.site.get("uncategorized_category_id") && @@ -80,7 +82,8 @@ export default Component.extend({ @discourseComputed("parentCategories") parentCategoriesSorted(parentCategories) { deprecated( - "The parentCategoriesSorted property of the bread-crumbs component is deprecated" + "The parentCategoriesSorted property of the bread-crumbs component is deprecated", + { id: "discourse.breadcrumbs.parentCategoriesSorted" } ); if (this.siteSettings.fixed_category_positions) { return parentCategories; @@ -97,7 +100,8 @@ export default Component.extend({ @discourseComputed("category", "parentCategory") firstCategory(category, parentCategory) { deprecated( - "The firstCategory property of the bread-crumbs component is deprecated" + "The firstCategory property of the bread-crumbs component is deprecated", + { id: "discourse.breadcrumbs.firstCategory" } ); return parentCategory || category; }, @@ -105,7 +109,8 @@ export default Component.extend({ @discourseComputed("category", "parentCategory") secondCategory(category, parentCategory) { deprecated( - "The secondCategory property of the bread-crumbs component is deprecated" + "The secondCategory property of the bread-crumbs component is deprecated", + { id: "discourse.breadcrumbs.secondCategory" } ); return parentCategory && category; }, @@ -113,7 +118,8 @@ export default Component.extend({ @discourseComputed("firstCategory", "hideSubcategories") childCategories(firstCategory, hideSubcategories) { deprecated( - "The childCategories property of the bread-crumbs component is deprecated" + "The childCategories property of the bread-crumbs component is deprecated", + { id: "discourse.breadcrumbs.childCategories" } ); if (hideSubcategories) { return []; diff --git a/app/assets/javascripts/discourse/app/components/bulk-select-all.js b/app/assets/javascripts/discourse/app/components/bulk-select-all.js deleted file mode 100644 index 63814b2e36..0000000000 --- a/app/assets/javascripts/discourse/app/components/bulk-select-all.js +++ /dev/null @@ -1,7 +0,0 @@ -import DButton from "discourse/components/d-button"; - -export default DButton.extend({ - click() { - $("input.bulk-select:not(checked)").click(); - }, -}); diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js index 545039b79d..f08549f397 100644 --- a/app/assets/javascripts/discourse/app/components/composer-editor.js +++ b/app/assets/javascripts/discourse/app/components/composer-editor.js @@ -10,6 +10,7 @@ import { } from "discourse/lib/utilities"; import discourseComputed, { bind, + debounce, observes, on, } from "discourse-common/utils/decorators"; @@ -17,6 +18,10 @@ import { fetchUnseenHashtags, linkSeenHashtags, } from "discourse/lib/link-hashtags"; +import { + fetchUnseenHashtagsInContext, + linkSeenHashtagsInContext, +} from "discourse/lib/hashtag-autocomplete"; import { cannotSee, fetchUnseenMentions, @@ -118,6 +123,11 @@ export default Component.extend(ComposerUploadUppy, { uploadPreProcessors, uploadHandlers, + init() { + this._super(...arguments); + this.warnedCannotSeeMentions = []; + }, + @discourseComputed("composer.requiredCategoryMissing") replyPlaceholder(requiredCategoryMissing) { if (requiredCategoryMissing) { @@ -181,6 +191,10 @@ export default Component.extend(ComposerUploadUppy, { } } }, + + hashtagTypesInPriorityOrder: + this.site.hashtag_configurations["topic-composer"], + hashtagIcons: this.site.hashtag_icons, }; }, @@ -471,11 +485,24 @@ export default Component.extend(ComposerUploadUppy, { }, _renderUnseenHashtags(preview) { - const unseen = linkSeenHashtags(preview); + let unseen; + const hashtagContext = this.site.hashtag_configurations["topic-composer"]; + if (this.siteSettings.enable_experimental_hashtag_autocomplete) { + unseen = linkSeenHashtagsInContext(hashtagContext, preview); + } else { + unseen = linkSeenHashtags(preview); + } + if (unseen.length > 0) { - fetchUnseenHashtags(unseen).then(() => { - linkSeenHashtags(preview); - }); + if (this.siteSettings.enable_experimental_hashtag_autocomplete) { + fetchUnseenHashtagsInContext(hashtagContext, unseen).then(() => { + linkSeenHashtagsInContext(hashtagContext, preview); + }); + } else { + fetchUnseenHashtags(unseen).then(() => { + linkSeenHashtags(preview); + }); + } } }, @@ -504,41 +531,30 @@ export default Component.extend(ComposerUploadUppy, { }); }, + // add a delay to allow for typing, so you don't open the warning right away + // previously we would warn after @bob even if you were about to mention @bob2 + @debounce(2000) _warnCannotSeeMention(preview) { - const composerDraftKey = this.get("composer.draftKey"); - - if (composerDraftKey === Composer.NEW_PRIVATE_MESSAGE_KEY) { + if (this.composer.draftKey === Composer.NEW_PRIVATE_MESSAGE_KEY) { return; } - schedule("afterRender", () => { - let found = this.warnedCannotSeeMentions || []; + const warnings = []; - preview?.querySelectorAll(".mention.cannot-see")?.forEach((mention) => { - let name = mention.dataset.name; + preview.querySelectorAll(".mention.cannot-see").forEach((mention) => { + const { name } = mention.dataset; - if (!found.includes(name)) { - // add a delay to allow for typing, so you don't open the warning right away - // previously we would warn after @bob even if you were about to mention @bob2 - discourseLater( - this, - () => { - if ( - preview?.querySelectorAll( - `.mention.cannot-see[data-name="${name}"]` - )?.length > 0 - ) { - this.cannotSeeMention([{ name, reason: cannotSee[name] }]); - found.push(name); - } - }, - 2000 - ); - } - }); + if (this.warnedCannotSeeMentions.includes(name)) { + return; + } - this.set("warnedCannotSeeMentions", found); + this.warnedCannotSeeMentions.push(name); + warnings.push({ name, reason: cannotSee[name] }); }); + + if (warnings.length > 0) { + this.cannotSeeMention(warnings); + } }, _warnHereMention(hereCount) { @@ -863,8 +879,14 @@ export default Component.extend(ComposerUploadUppy, { this._warnMentionedGroups(preview); this._warnCannotSeeMention(preview); - // Paint category and tag hashtags - const unseenHashtags = linkSeenHashtags(preview); + // Paint category, tag, and other data source hashtags + let unseenHashtags; + const hashtagContext = this.site.hashtag_configurations["topic-composer"]; + if (this.siteSettings.enable_experimental_hashtag_autocomplete) { + unseenHashtags = linkSeenHashtagsInContext(hashtagContext, preview); + } else { + unseenHashtags = linkSeenHashtags(preview); + } if (unseenHashtags.length > 0) { discourseDebounce(this, this._renderUnseenHashtags, preview, 450); } diff --git a/app/assets/javascripts/discourse/app/components/composer-message.js b/app/assets/javascripts/discourse/app/components/composer-message.js index 2fd8055775..86483921f2 100644 --- a/app/assets/javascripts/discourse/app/components/composer-message.js +++ b/app/assets/javascripts/discourse/app/components/composer-message.js @@ -14,7 +14,8 @@ export default Component.extend({ actions: { closeMessage() { deprecated( - 'You should use `action=(closeMessage message)` instead of `action=(action "closeMessage")`' + 'You should use `action=(closeMessage message)` instead of `action=(action "closeMessage")`', + { id: "discourse.composer-message.closeMessage" } ); this.closeMessage(this.message); }, diff --git a/app/assets/javascripts/discourse/app/components/composer-save-button.hbs b/app/assets/javascripts/discourse/app/components/composer-save-button.hbs new file mode 100644 index 0000000000..f2c94cf4e4 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/composer-save-button.hbs @@ -0,0 +1,9 @@ + diff --git a/app/assets/javascripts/discourse/app/components/composer-save-button.js b/app/assets/javascripts/discourse/app/components/composer-save-button.js index 03200017e9..e3b6178c88 100644 --- a/app/assets/javascripts/discourse/app/components/composer-save-button.js +++ b/app/assets/javascripts/discourse/app/components/composer-save-button.js @@ -1,10 +1,9 @@ -import Button from "discourse/components/d-button"; +import Component from "@glimmer/component"; import I18n from "I18n"; import { translateModKey } from "discourse/lib/utilities"; -export default Button.extend({ - classNameBindings: [":btn-primary", ":create", "disableSubmit:disabled"], - translatedTitle: I18n.t("composer.title", { - modifier: translateModKey("Meta+"), - }), -}); +export default class ComposerSaveButton extends Component { + get translatedTitle() { + return I18n.t("composer.title", { modifier: translateModKey("Meta+") }); + } +} diff --git a/app/assets/javascripts/discourse/app/components/create-account.js b/app/assets/javascripts/discourse/app/components/create-account.js index 36ac076d18..745cd75ef3 100644 --- a/app/assets/javascripts/discourse/app/components/create-account.js +++ b/app/assets/javascripts/discourse/app/components/create-account.js @@ -6,19 +6,19 @@ export default Component.extend({ classNames: ["create-account-body"], userInputFocus(event) { - let label = event.target.parentElement.previousElementSibling; - if (!label.classList.contains("value-entered")) { - label.classList.toggle("value-entered"); + const controls = event.target.parentElement; + if (!controls.classList.contains("value-entered")) { + controls.classList.toggle("value-entered"); } }, userInputFocusOut(event) { - let label = event.target.parentElement.previousElementSibling; + const controls = event.target.parentElement; if ( event.target.value.length === 0 && - label.classList.contains("value-entered") + controls.classList.contains("value-entered") ) { - label.classList.toggle("value-entered"); + controls.classList.toggle("value-entered"); } }, diff --git a/app/assets/javascripts/discourse/app/components/d-button.js b/app/assets/javascripts/discourse/app/components/d-button.js index b2f8d2ccb1..248c129e5c 100644 --- a/app/assets/javascripts/discourse/app/components/d-button.js +++ b/app/assets/javascripts/discourse/app/components/d-button.js @@ -1,161 +1,137 @@ import { inject as service } from "@ember/service"; +import { action } from "@ember/object"; import { empty, equal, notEmpty } from "@ember/object/computed"; -import Component from "@ember/component"; +import GlimmerComponentWithDeprecatedParentView from "discourse/components/glimmer-component-with-deprecated-parent-view"; +import deprecated from "discourse-common/lib/deprecated"; import DiscourseURL from "discourse/lib/url"; import I18n from "I18n"; -import { computed } from "@ember/object"; -import discourseComputed from "discourse-common/utils/decorators"; -export default Component.extend({ - tagName: "button", - // subclasses need this - layoutName: "components/d-button", - form: null, - type: "button", - title: null, - translatedTitle: null, - label: null, - translatedLabel: null, - ariaLabel: null, - ariaExpanded: null, - ariaControls: null, - translatedAriaLabel: null, - forwardEvent: false, - preventFocus: false, - onKeyDown: null, - router: service(), +const ACTION_AS_STRING_DEPRECATION_ARGS = [ + "DButton no longer supports @action as a string. Please refactor to use an closure action instead.", + { id: "discourse.d-button-action-string" }, +]; - isLoading: computed({ - set(key, value) { - this.set("forceDisabled", !!value); - return value; - }, - }), +export default class DButton extends GlimmerComponentWithDeprecatedParentView { + @service router; - classNameBindings: [ - "isLoading:is-loading", - "btnLink::btn", - "btnLink", - "noText", - "btnType", - ], - attributeBindings: [ - "form", - "isDisabled:disabled", - "computedTitle:title", - "computedAriaLabel:aria-label", - "computedAriaExpanded:aria-expanded", - "ariaControls:aria-controls", - "tabindex", - "type", - ], + @notEmpty("args.icon") + btnIcon; - isDisabled: computed("disabled", "forceDisabled", function () { - return this.forceDisabled || this.disabled; - }), + @equal("args.display", "link") + btnLink; - forceDisabled: false, + @empty("computedLabel") + noText; - btnIcon: notEmpty("icon"), + constructor() { + super(...arguments); + if (typeof this.args.action === "string") { + deprecated(...ACTION_AS_STRING_DEPRECATION_ARGS); + } + } - btnLink: equal("display", "link"), + get forceDisabled() { + return !!this.args.isLoading; + } - @discourseComputed("icon", "computedLabel") - btnType(icon, translatedLabel) { - if (icon) { - return translatedLabel ? "btn-icon-text" : "btn-icon"; - } else if (translatedLabel) { + get isDisabled() { + return this.forceDisabled || this.args.disabled; + } + + get btnType() { + if (this.args.icon) { + return this.computedLabel ? "btn-icon-text" : "btn-icon"; + } else if (this.computedLabel) { return "btn-text"; } - }, + } - noText: empty("computedLabel"), - - @discourseComputed("title", "translatedTitle") - computedTitle(title, translatedTitle) { - if (title) { - return I18n.t(title); + get computedTitle() { + if (this.args.title) { + return I18n.t(this.args.title); } - return translatedTitle; - }, + return this.args.translatedTitle; + } - @discourseComputed("label", "translatedLabel") - computedLabel(label, translatedLabel) { - if (label) { - return I18n.t(label); + get computedLabel() { + if (this.args.label) { + return I18n.t(this.args.label); } - return translatedLabel; - }, + return this.args.translatedLabel; + } - @discourseComputed("ariaLabel", "translatedAriaLabel") - computedAriaLabel(ariaLabel, translatedAriaLabel) { - if (ariaLabel) { - return I18n.t(ariaLabel); + get computedAriaLabel() { + if (this.args.ariaLabel) { + return I18n.t(this.args.ariaLabel); } - if (translatedAriaLabel) { - return translatedAriaLabel; + if (this.args.translatedAriaLabel) { + return this.args.translatedAriaLabel; } - }, + } - @discourseComputed("ariaExpanded") - computedAriaExpanded(ariaExpanded) { - if (ariaExpanded === true) { + get computedAriaExpanded() { + if (this.args.ariaExpanded === true) { return "true"; } - if (ariaExpanded === false) { + if (this.args.ariaExpanded === false) { return "false"; } - }, + } + @action keyDown(e) { - if (this.onKeyDown) { + if (this.args.onKeyDown) { e.stopPropagation(); - this.onKeyDown(e); + this.args.onKeyDown(e); } else if (e.key === "Enter") { this._triggerAction(e); - return false; } - }, + } + @action click(event) { return this._triggerAction(event); - }, + } + @action mouseDown(event) { - if (this.preventFocus) { + if (this.args.preventFocus) { event.preventDefault(); } - }, + } _triggerAction(event) { - let { action, route, href } = this; + const { action: actionVal, route, href } = this.args; - if (action || route || href?.length) { - if (action) { - if (typeof action === "string") { - // Note: This is deprecated in new Embers and needs to be removed in the future. - // There is already a warning in the console. - this.sendAction("action", this.actionParam); - } else if (typeof action === "object" && action.value) { - if (this.forwardEvent) { - action.value(this.actionParam, event); + if (actionVal || route || href?.length) { + if (actionVal) { + const { actionParam, forwardEvent } = this.args; + + if (typeof actionVal === "string") { + deprecated(...ACTION_AS_STRING_DEPRECATION_ARGS); + if (this._target?.send) { + this._target.send(actionVal, actionParam); } else { - action.value(this.actionParam); + throw new Error( + "DButton could not find a target for the action. Use a closure action instead" + ); } - } else if (typeof this.action === "function") { - if (this.forwardEvent) { - action(this.actionParam, event); + } else if (typeof actionVal === "object" && actionVal.value) { + if (forwardEvent) { + actionVal.value(actionParam, event); } else { - action(this.actionParam); + actionVal.value(actionParam); + } + } else if (typeof actionVal === "function") { + if (forwardEvent) { + actionVal(actionParam, event); + } else { + actionVal(actionParam); } } - } - - if (route) { + } else if (route) { this.router.transitionTo(route); - } - - if (href?.length) { + } else if (href?.length) { DiscourseURL.routeTo(href); } @@ -164,5 +140,5 @@ export default Component.extend({ return false; } - }, -}); + } +} diff --git a/app/assets/javascripts/discourse/app/components/d-editor.js b/app/assets/javascripts/discourse/app/components/d-editor.js index 298dc542ec..64175e030f 100644 --- a/app/assets/javascripts/discourse/app/components/d-editor.js +++ b/app/assets/javascripts/discourse/app/components/d-editor.js @@ -209,7 +209,9 @@ export function clearToolbarCallbacks() { } export function onToolbarCreate(func) { - deprecated("`onToolbarCreate` is deprecated, use the plugin api instead."); + deprecated("`onToolbarCreate` is deprecated, use the plugin api instead.", { + id: "discourse.d-editor.on-toolbar-create", + }); addToolbarCallback(func); } @@ -256,7 +258,7 @@ export default Component.extend(TextareaTextManipulation, { this._textarea = this.element.querySelector("textarea.d-editor-input"); this._$textarea = $(this._textarea); this._applyEmojiAutocomplete(this._$textarea); - this._applyCategoryHashtagAutocomplete(this._$textarea); + this._applyHashtagAutocomplete(this._$textarea); scheduleOnce("afterRender", this, this._readyNow); @@ -457,9 +459,9 @@ export default Component.extend(TextareaTextManipulation, { } }, - _applyCategoryHashtagAutocomplete() { + _applyHashtagAutocomplete() { setupHashtagAutocomplete( - "topic-composer", + this.site.hashtag_configurations["topic-composer"], this._$textarea, this.siteSettings, (value) => { diff --git a/app/assets/javascripts/discourse/app/components/d-section.js b/app/assets/javascripts/discourse/app/components/d-section.js index 91bc8bfaad..515f4b5ed7 100644 --- a/app/assets/javascripts/discourse/app/components/d-section.js +++ b/app/assets/javascripts/discourse/app/components/d-section.js @@ -18,6 +18,7 @@ export default class extends Component { deprecated("Uses boolean instead of string for scrollTop.", { since: "2.8.0.beta9", dropFrom: "2.9.0.beta1", + id: "discourse.d-section.scroll-top-boolean", }); return; diff --git a/app/assets/javascripts/discourse/app/components/glimmer-component-with-deprecated-parent-view.js b/app/assets/javascripts/discourse/app/components/glimmer-component-with-deprecated-parent-view.js new file mode 100644 index 0000000000..b144262a5d --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/glimmer-component-with-deprecated-parent-view.js @@ -0,0 +1,46 @@ +import Component from "@glimmer/component"; +import { + CustomComponentManager, + setInternalComponentManager, +} from "@glimmer/manager"; +import EmberGlimmerComponentManager from "@glimmer/component/-private/ember-component-manager"; +import { valueForRef } from "@glimmer/reference"; + +class GlimmerComponentWithParentViewManager extends CustomComponentManager { + create( + owner, + componentClass, + args, + environment, + dynamicScope, + callerSelfRef + ) { + const result = super.create(...arguments); + + result.component.parentView = dynamicScope.view; + dynamicScope.view = result.component; + + result.component._target = valueForRef(callerSelfRef); + + return result; + } + + getCapabilities() { + return { ...super.getCapabilities(), createCaller: true }; + } +} + +/** + * This component has a lightly-extended version of Ember's default Glimmer component manager. + * It gives Glimmer components the ability to reference their parent view which can be useful + * when building backwards-compatible versions of components. Any use of the parentView property + * of the component should be considered deprecated. + */ +export default class GlimmerComponentWithDeprecatedParentView extends Component {} + +setInternalComponentManager( + new GlimmerComponentWithParentViewManager( + (owner) => new EmberGlimmerComponentManager(owner) + ), + GlimmerComponentWithDeprecatedParentView +); diff --git a/app/assets/javascripts/discourse/app/components/header-extra-info.js b/app/assets/javascripts/discourse/app/components/header-extra-info.js index 81151b67e2..51977030dd 100644 --- a/app/assets/javascripts/discourse/app/components/header-extra-info.js +++ b/app/assets/javascripts/discourse/app/components/header-extra-info.js @@ -2,6 +2,7 @@ import deprecated from "discourse-common/lib/deprecated"; export function needsSecondRowIf() { deprecated( - "`needsSecondRowIf` is deprecated. Use widget hooks on `header-second-row`" + "`needsSecondRowIf` is deprecated. Use widget hooks on `header-second-row`", + { id: "discourse.header-extra-info.needs-second-row-if" } ); } diff --git a/app/assets/javascripts/discourse/app/components/highlight-text.js b/app/assets/javascripts/discourse/app/components/highlight-text.js index 6199140de3..9d6f962625 100644 --- a/app/assets/javascripts/discourse/app/components/highlight-text.js +++ b/app/assets/javascripts/discourse/app/components/highlight-text.js @@ -5,7 +5,8 @@ export default highlightSearch.extend({ init() { this._super(...arguments); deprecated( - "`highlight-text` component is deprecated, use the `highlight-search` instead." + "`highlight-text` component is deprecated, use the `highlight-search` instead.", + { id: "discourse.highlight-text-component" } ); }, }); diff --git a/app/assets/javascripts/discourse/app/components/html-with-links.js b/app/assets/javascripts/discourse/app/components/html-with-links.js index a49b1dfcc9..9149668fb8 100644 --- a/app/assets/javascripts/discourse/app/components/html-with-links.js +++ b/app/assets/javascripts/discourse/app/components/html-with-links.js @@ -8,7 +8,7 @@ export default Component.extend({ click(event) { if (event?.target?.tagName === "A") { if (shouldOpenInNewTab(event.target.href)) { - openLinkInNewTab(event.target); + openLinkInNewTab(event, event.target); } } }, diff --git a/app/assets/javascripts/discourse/app/components/login-reply-button.js b/app/assets/javascripts/discourse/app/components/login-reply-button.js deleted file mode 100644 index edc71d444d..0000000000 --- a/app/assets/javascripts/discourse/app/components/login-reply-button.js +++ /dev/null @@ -1,7 +0,0 @@ -import Button from "discourse/components/d-button"; - -export default Button.extend({ - label: "topic.reply.title", - icon: "reply", - action: "showLogin", -}); diff --git a/app/assets/javascripts/discourse/app/components/mobile-nav.js b/app/assets/javascripts/discourse/app/components/mobile-nav.js index f0ea3e6138..1d4b93a4a0 100644 --- a/app/assets/javascripts/discourse/app/components/mobile-nav.js +++ b/app/assets/javascripts/discourse/app/components/mobile-nav.js @@ -19,6 +19,7 @@ export default Component.extend({ deprecated("{{mobile-nav}} no longer requires the currentPath property", { since: "2.7.0.beta4", dropFrom: "2.9.0.beta1", + id: "discourse.mobile-nav.currentPath", }); } }, diff --git a/app/assets/javascripts/discourse/app/components/plugin-connector.js b/app/assets/javascripts/discourse/app/components/plugin-connector.js index 22f5252e05..42f829efbf 100644 --- a/app/assets/javascripts/discourse/app/components/plugin-connector.js +++ b/app/assets/javascripts/discourse/app/components/plugin-connector.js @@ -39,7 +39,10 @@ export default Component.extend({ key, computed("deprecatedArgs", () => { deprecated( - `The ${key} property is deprecated, but is being used in ${this.layoutName}` + `The ${key} property is deprecated, but is being used in ${this.layoutName}`, + { + id: "discourse.plugin-connector.deprecated-arg", + } ); return (this.deprecatedArgs || {})[key]; diff --git a/app/assets/javascripts/discourse/app/components/sidebar.hbs b/app/assets/javascripts/discourse/app/components/sidebar.hbs index 65d3b9240f..533f7531a1 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar.hbs +++ b/app/assets/javascripts/discourse/app/components/sidebar.hbs @@ -1,4 +1,4 @@ - + diff --git a/app/assets/javascripts/discourse/app/components/sidebar/section-header.hbs b/app/assets/javascripts/discourse/app/components/sidebar/section-header.hbs index 55839c39e3..b878399fea 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/section-header.hbs +++ b/app/assets/javascripts/discourse/app/components/sidebar/section-header.hbs @@ -2,8 +2,9 @@ - + @action={{@toggleSectionDisplay}} + @ariaExpanded={{@isExpanded}} + @ariaControls={{@sidebarSectionContentID}} > {{yield}} {{else}} diff --git a/app/assets/javascripts/discourse/app/components/sidebar/section-link-prefix.hbs b/app/assets/javascripts/discourse/app/components/sidebar/section-link-prefix.hbs index 31c5ec6748..15c23a8b6f 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/section-link-prefix.hbs +++ b/app/assets/javascripts/discourse/app/components/sidebar/section-link-prefix.hbs @@ -1,5 +1,5 @@ {{#if @prefixType}} - + {{#if (eq @prefixType "image")}} {{/if}} diff --git a/app/assets/javascripts/discourse/app/components/sidebar/section-link.hbs b/app/assets/javascripts/discourse/app/components/sidebar/section-link.hbs index c29385e2c0..d1d9cb9451 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/section-link.hbs +++ b/app/assets/javascripts/discourse/app/components/sidebar/section-link.hbs @@ -1,5 +1,5 @@ {{#if this.shouldDisplay}} - + {{/if}} diff --git a/app/assets/javascripts/discourse/app/components/sidebar/section-link.js b/app/assets/javascripts/discourse/app/components/sidebar/section-link.js index e68e31c0d0..620da26bd6 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/section-link.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/section-link.js @@ -7,6 +7,12 @@ export default class SectionLink extends Component { } } + didInsert(_element, [args]) { + if (args.didInsert) { + args.didInsert(); + } + } + get shouldDisplay() { if (this.args.shouldDisplay === undefined) { return true; diff --git a/app/assets/javascripts/discourse/app/components/sidebar/section.hbs b/app/assets/javascripts/discourse/app/components/sidebar/section.hbs index de74d2ccdf..047a97b1b5 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/section.hbs +++ b/app/assets/javascripts/discourse/app/components/sidebar/section.hbs @@ -1,7 +1,12 @@ {{#if this.displaySection}}
{{#if this.displaySectionContent}} - + {{/if}}
{{/if}} diff --git a/app/assets/javascripts/discourse/app/components/sidebar/section.js b/app/assets/javascripts/discourse/app/components/sidebar/section.js index ea11c34d95..6b227a67e0 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/section.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/section.js @@ -8,6 +8,7 @@ export default class SidebarSection extends Component { @service keyValueStore; @tracked displaySectionContent; + sidebarSectionContentID = `sidebar-section-content-${this.args.sectionName}`; collapsedSidebarSectionKey = `sidebar-section-${this.args.sectionName}-collapsed`; constructor() { diff --git a/app/assets/javascripts/discourse/app/components/sidebar/user/community-section.js b/app/assets/javascripts/discourse/app/components/sidebar/user/community-section.js index 1191981a20..2bcb92da48 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/user/community-section.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/user/community-section.js @@ -4,7 +4,6 @@ import Composer from "discourse/models/composer"; import { getOwner } from "discourse-common/lib/get-owner"; import PermissionType from "discourse/models/permission-type"; import EverythingSectionLink from "discourse/lib/sidebar/common/community-section/everything-section-link"; -import TrackedSectionLink from "discourse/lib/sidebar/user/community-section/tracked-section-link"; import MyPostsSectionLink from "discourse/lib/sidebar/user/community-section/my-posts-section-link"; import GroupsSectionLink from "discourse/lib/sidebar/common/community-section/groups-section-link"; import UsersSectionLink from "discourse/lib/sidebar/common/community-section/users-section-link"; @@ -35,7 +34,6 @@ export default class SidebarUserCommunitySection extends SidebarCommonCommunityS get defaultMainSectionLinks() { return [ EverythingSectionLink, - TrackedSectionLink, MyPostsSectionLink, AdminSectionLink, ReviewSectionLink, diff --git a/app/assets/javascripts/discourse/app/components/sidebar/user/sections.hbs b/app/assets/javascripts/discourse/app/components/sidebar/user/sections.hbs index 0a47f55149..41dfff5a6d 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/user/sections.hbs +++ b/app/assets/javascripts/discourse/app/components/sidebar/user/sections.hbs @@ -43,6 +43,7 @@ @hoverAction={{link.hoverAction}} @hoverTitle={{link.hoverTitle}} @currentWhen={{link.currentWhen}} + @didInsert={{link.didInsert}} @willDestroy={{link.willDestroy}} @content={{link.text}} /> {{/each}} diff --git a/app/assets/javascripts/discourse/app/components/site-header.js b/app/assets/javascripts/discourse/app/components/site-header.js index 4248d31adf..ceba5d0d9e 100644 --- a/app/assets/javascripts/discourse/app/components/site-header.js +++ b/app/assets/javascripts/discourse/app/components/site-header.js @@ -40,6 +40,20 @@ const SiteHeaderComponent = MountWidget.extend( this.queueRerender(); }, + @observes("site.narrowDesktopView") + narrowDesktopViewChanged() { + if ( + this.siteSettings.enable_experimental_sidebar_hamburger && + (!this.sidebarEnabled || this.site.narrowDesktopView) + ) { + this.appEvents.on( + "sidebar-hamburger-dropdown:rendered", + this, + "_animateMenu" + ); + } + }, + _animateOpening(panel) { const headerCloak = document.querySelector(".header-cloak"); panel.classList.add("animate"); @@ -219,7 +233,7 @@ const SiteHeaderComponent = MountWidget.extend( if ( this.siteSettings.enable_experimental_sidebar_hamburger && - !this.sidebarEnabled + (!this.sidebarEnabled || this.site.narrowDesktopView) ) { this.appEvents.on( "sidebar-hamburger-dropdown:rendered", @@ -238,43 +252,6 @@ const SiteHeaderComponent = MountWidget.extend( this.currentUser.on("status-changed", this, "queueRerender"); } - if (!this.siteSettings.enable_user_tips) { - if ( - this.currentUser && - !this.get("currentUser.read_first_notification") - ) { - document.body.classList.add("unread-first-notification"); - } - - // Allow first notification to be dismissed on a click anywhere - if ( - this.currentUser && - !this.get("currentUser.read_first_notification") && - !this.get("currentUser.enforcedSecondFactor") - ) { - this._dismissFirstNotification = (e) => { - if (document.body.classList.contains("unread-first-notification")) { - document.body.classList.remove("unread-first-notification"); - } - if ( - !e.target.closest("#current-user") && - !e.target.closest(".ring-backdrop") && - this.currentUser && - !this.get("currentUser.read_first_notification") && - !this.get("currentUser.enforcedSecondFactor") - ) { - this.eventDispatched( - "header:dismiss-first-notification-mask", - "header" - ); - } - }; - document.addEventListener("click", this._dismissFirstNotification, { - once: true, - }); - } - } - const header = document.querySelector("header.d-header"); this._itsatrap = new ItsATrap(header); const dirs = this.currentUser?.redesigned_user_menu_enabled @@ -365,8 +342,6 @@ const SiteHeaderComponent = MountWidget.extend( this._itsatrap?.destroy(); this._itsatrap = null; - - document.removeEventListener("click", this._dismissFirstNotification); }, buildArgs() { @@ -374,6 +349,7 @@ const SiteHeaderComponent = MountWidget.extend( topic: this._topic, canSignUp: this.canSignUp, sidebarEnabled: this.sidebarEnabled, + showSidebar: this.showSidebar, }; }, @@ -391,14 +367,17 @@ const SiteHeaderComponent = MountWidget.extend( const menuPanels = document.querySelectorAll(".menu-panel"); if (menuPanels.length === 0) { - if (this.site.mobileView) { + if (this.site.mobileView || this.site.narrowDesktopView) { this._animate = true; } return; } const windowWidth = document.body.offsetWidth; - const viewMode = this.site.mobileView ? "slide-in" : "drop-down"; + const viewMode = + this.site.mobileView || this.site.narrowDesktopView + ? "slide-in" + : "drop-down"; menuPanels.forEach((panel) => { const headerCloak = document.querySelector(".header-cloak"); @@ -415,7 +394,7 @@ const SiteHeaderComponent = MountWidget.extend( panel.classList.add(viewMode); if (this._animate || this._panMenuOffset !== 0) { if ( - this.site.mobileView && + (this.site.mobileView || this.site.narrowDesktopView) && panel.parentElement.classList.contains(this._leftMenuClass()) ) { this._panMenuOrigin = "left"; @@ -432,7 +411,7 @@ const SiteHeaderComponent = MountWidget.extend( // We use a mutationObserver to check for style changes, so it's important // we don't set it if it doesn't change. Same goes for the panelBody! - if (!this.site.mobileView) { + if (!this.site.mobileView && !this.site.narrowDesktopView) { const buttonPanel = document.querySelectorAll("header ul.icons"); if (buttonPanel.length === 0) { return; @@ -507,6 +486,8 @@ export default SiteHeaderComponent.extend({ didInsertElement() { this._super(...arguments); + this.appEvents.on("site-header:force-refresh", this, "queueRerender"); + const header = document.querySelector(".d-header-wrap"); if (header) { schedule("afterRender", () => { @@ -537,6 +518,7 @@ export default SiteHeaderComponent.extend({ this._super(...arguments); this._resizeObserver?.disconnect(); + this.appEvents.off("site-header:force-refresh", this, "queueRerender"); }, }); diff --git a/app/assets/javascripts/discourse/app/components/text-field.js b/app/assets/javascripts/discourse/app/components/text-field.js index d3fd889b12..9044d9c1ce 100644 --- a/app/assets/javascripts/discourse/app/components/text-field.js +++ b/app/assets/javascripts/discourse/app/components/text-field.js @@ -53,12 +53,13 @@ export default TextField.extend({ next(() => this.onChange(this.value)); }, - @discourseComputed - dir() { + get dir() { if (this.siteSettings.support_mixed_text_direction) { - let val = this.value; - if (val) { - return isRTL(val) ? "rtl" : "ltr"; + const val = this.get("value"); + if (val && isRTL(val)) { + return "rtl"; + } else if (val && isLTR(val)) { + return "ltr"; } else { return siteDir(); } @@ -70,21 +71,6 @@ export default TextField.extend({ cancel(this._timer); }, - keyUp(event) { - this._super(event); - - if (this.siteSettings.support_mixed_text_direction) { - let val = this.value; - if (isRTL(val)) { - this.set("dir", "rtl"); - } else if (isLTR(val)) { - this.set("dir", "ltr"); - } else { - this.set("dir", siteDir()); - } - } - }, - @discourseComputed("placeholderKey") placeholder: { get() { diff --git a/app/assets/javascripts/discourse/app/components/topic-footer-buttons.js b/app/assets/javascripts/discourse/app/components/topic-footer-buttons.js index e65a85aeb8..5f91ecca8e 100644 --- a/app/assets/javascripts/discourse/app/components/topic-footer-buttons.js +++ b/app/assets/javascripts/discourse/app/components/topic-footer-buttons.js @@ -2,6 +2,7 @@ import { alias, or } from "@ember/object/computed"; import { computed } from "@ember/object"; import Component from "@ember/component"; import discourseComputed from "discourse-common/utils/decorators"; +import { NotificationLevels } from "discourse/lib/notification-levels"; import { getTopicFooterButtons } from "discourse/lib/register-topic-footer-button"; import { getTopicFooterDropdowns } from "discourse/lib/register-topic-footer-dropdown"; @@ -46,6 +47,11 @@ export default Component.extend({ return !isPM || this.canSendPms; }, + @discourseComputed("topic.details.notification_level") + showNotificationUserTip(notificationLevel) { + return notificationLevel >= NotificationLevels.TRACKING; + }, + canSendPms: alias("currentUser.can_send_private_messages"), canInviteTo: alias("topic.details.can_invite_to"), diff --git a/app/assets/javascripts/discourse/app/components/topic-list.js b/app/assets/javascripts/discourse/app/components/topic-list.js index 19fb1a283d..6a1d1bd519 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list.js +++ b/app/assets/javascripts/discourse/app/components/topic-list.js @@ -12,7 +12,7 @@ export default Component.extend(LoadMore, { classNameBindings: ["bulkSelectEnabled:sticky-header"], showTopicPostBadges: true, listTitle: "topic.title", - canDoBulkActions: and("currentUser.staff", "selected.length"), + canDoBulkActions: and("currentUser.canManageTopic", "selected.length"), // Overwrite this to perform client side filtering of topics, if desired filteredTopics: alias("topics"), diff --git a/app/assets/javascripts/discourse/app/components/user-menu/likes-list-empty-state.hbs b/app/assets/javascripts/discourse/app/components/user-menu/likes-list-empty-state.hbs new file mode 100644 index 0000000000..9ba1bebe4a --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-menu/likes-list-empty-state.hbs @@ -0,0 +1,10 @@ +
+ + {{i18n "user.no_likes_title"}} + +
+

+ {{html-safe (i18n "user.no_likes_body" preferencesUrl=(get-url "/my/preferences/notifications"))}} +

+
+
diff --git a/app/assets/javascripts/discourse/app/components/user-menu/likes-notifications-list.js b/app/assets/javascripts/discourse/app/components/user-menu/likes-notifications-list.js index 2e2ad287ef..fd87ffe3e8 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/likes-notifications-list.js +++ b/app/assets/javascripts/discourse/app/components/user-menu/likes-notifications-list.js @@ -8,4 +8,8 @@ export default class UserMenuLikesNotificationsList extends UserMenuNotification dismissWarningModal() { return null; } + + get emptyStateComponent() { + return "user-menu/likes-list-empty-state"; + } } diff --git a/app/assets/javascripts/discourse/app/components/user-nav.hbs b/app/assets/javascripts/discourse/app/components/user-nav.hbs index 4caa7d2e7a..f4a2757cae 100644 --- a/app/assets/javascripts/discourse/app/components/user-nav.hbs +++ b/app/assets/javascripts/discourse/app/components/user-nav.hbs @@ -28,7 +28,7 @@ {{#if @showPrivateMessages}}
  • - {{d-icon "far-envelope"}} + {{d-icon "envelope"}} {{i18n "user.private_messages"}}
  • diff --git a/app/assets/javascripts/discourse/app/components/user-nav/messages-nav.hbs b/app/assets/javascripts/discourse/app/components/user-nav/messages-nav.hbs index 565e6c336c..622d9823b0 100644 --- a/app/assets/javascripts/discourse/app/components/user-nav/messages-nav.hbs +++ b/app/assets/javascripts/discourse/app/components/user-nav/messages-nav.hbs @@ -7,7 +7,7 @@ {{/if}} - + {{#if @isPersonal}}
  • @@ -47,39 +47,37 @@
  • {{/if}} - {{#each @user.groups as |group|}} - {{#if (and @isGroup (eq @groupFilter group.name))}} - {{#if @viewingSelf}} -
  • - - {{d-icon "envelope"}} - {{i18n "categories.latest"}} - -
  • + {{#if @isGroup}} +
  • + + {{d-icon "envelope"}} + {{i18n "categories.latest"}} + +
  • -
  • - - {{d-icon "exclamation-circle"}} - {{@newLinkText}} - -
  • + {{#if @viewingSelf}} +
  • + + {{d-icon "exclamation-circle"}} + {{@newLinkText}} + +
  • -
  • - - {{d-icon "plus-circle"}} - {{@unreadLinkText}} - -
  • - {{/if}} +
  • + + {{d-icon "plus-circle"}} + {{@unreadLinkText}} + +
  • - + {{d-icon "archive"}} {{i18n "user.messages.archive"}}
  • {{/if}} - {{/each}} + {{/if}} {{#if this.displayTags}}
  • diff --git a/app/assets/javascripts/discourse/app/components/user-nav/preferences-nav.hbs b/app/assets/javascripts/discourse/app/components/user-nav/preferences-nav.hbs index 29e1c35c5d..45a007b6c3 100644 --- a/app/assets/javascripts/discourse/app/components/user-nav/preferences-nav.hbs +++ b/app/assets/javascripts/discourse/app/components/user-nav/preferences-nav.hbs @@ -4,58 +4,58 @@ {{i18n "user.preferences_nav.account"}}
  • + + + + + {{#if @model.can_change_tracking_preferences}} - {{/if}} + -{{#if (and @model.can_change_tracking_preferences @siteSettings.tagging_enabled)}} - -{{/if}} + + {{#if @siteSettings.enable_experimental_sidebar_hamburger}} {{/if}} + - diff --git a/app/assets/javascripts/discourse/app/components/user-preferences/allow-private-messages.hbs b/app/assets/javascripts/discourse/app/components/user-preferences/allow-private-messages.hbs new file mode 100644 index 0000000000..82afce8d81 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-preferences/allow-private-messages.hbs @@ -0,0 +1,6 @@ +
    + +
    + +
    +
    diff --git a/app/assets/javascripts/discourse/app/components/user-preferences/categories.hbs b/app/assets/javascripts/discourse/app/components/user-preferences/categories.hbs new file mode 100644 index 0000000000..3b36738f5d --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-preferences/categories.hbs @@ -0,0 +1,53 @@ +
    + + + +
    {{i18n "user.watched_categories_instructions"}}
    + +
    + + {{#if @canSee}} + {{i18n "user.tracked_topics_link"}} + {{/if}} + +
    +
    {{i18n "user.tracked_categories_instructions"}}
    + +
    + + +
    +
    {{i18n "user.watched_first_post_categories_instructions"}}
    + + {{#if @siteSettings.mute_all_categories_by_default}} +
    + + +
    +
    {{i18n "user.regular_categories_instructions"}}
    + {{else}} +
    + + + {{#if @canSee}} + {{i18n "user.tracked_topics_link"}} + {{/if}} + + +
    + +
    {{i18n (if @hideMutedTags "user.muted_categories_instructions" "user.muted_categories_instructions_dont_hide")}}
    + {{/if}} +
    + + + +
    + + diff --git a/app/assets/javascripts/discourse/app/components/user-preferences/tags.hbs b/app/assets/javascripts/discourse/app/components/user-preferences/tags.hbs new file mode 100644 index 0000000000..4f4568d545 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-preferences/tags.hbs @@ -0,0 +1,45 @@ +{{#if @siteSettings.tagging_enabled}} +
    + + +
    + + +
    + +
    {{i18n "user.watched_tags_instructions"}}
    + +
    + + +
    + +
    {{i18n "user.tracked_tags_instructions"}}
    + + + +
    + {{i18n "user.watched_first_post_tags_instructions"}} +
    + +
    + + +
    +
    {{i18n "user.muted_tags_instructions"}}
    +
    + + + +{{/if}} diff --git a/app/assets/javascripts/discourse/app/components/user-preferences/topic-tracking.hbs b/app/assets/javascripts/discourse/app/components/user-preferences/topic-tracking.hbs new file mode 100644 index 0000000000..75e772cb73 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-preferences/topic-tracking.hbs @@ -0,0 +1,16 @@ +
    +
    + + +
    + +
    + + +
    + +
    + + +
    +
    diff --git a/app/assets/javascripts/discourse/app/components/user-preferences/user-api-keys.hbs b/app/assets/javascripts/discourse/app/components/user-preferences/user-api-keys.hbs new file mode 100644 index 0000000000..9244ab4948 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-preferences/user-api-keys.hbs @@ -0,0 +1,35 @@ +{{#if @model.userApiKeys}} +
    + + +
    + {{#each @model.userApiKeys as |key|}} +
    + {{key.application_name}} + + {{#if key.revoked}} + + {{else}} + + {{/if}} + +

    +

      + {{#each key.scopes as |scope|}} +
    • {{scope}}
    • + {{/each}} +
    +

    + +

    + {{i18n "user.api_approved"}} {{bound-date key.created_at}} +

    + +

    + {{i18n "user.api_last_used_at"}} {{bound-date key.last_used_at}} +

    +
    + {{/each}} +
    +
    +{{/if}} diff --git a/app/assets/javascripts/discourse/app/components/user-selector.js b/app/assets/javascripts/discourse/app/components/user-selector.js index 4f84623fe5..17c85727db 100644 --- a/app/assets/javascripts/discourse/app/components/user-selector.js +++ b/app/assets/javascripts/discourse/app/components/user-selector.js @@ -16,8 +16,8 @@ export default TextField.extend({ @on("init") deprecateComponent() { deprecated( - "`{{user-selector}}` is deprecated. Please use `{{email-group-user-chooser}}` instead.", - { since: "2.7", dropFrom: "2.8" } + "The `` component is deprecated. Please use `` instead.", + { since: "2.7", dropFrom: "2.8", id: "discourse.user-selector-component" } ); }, diff --git a/app/assets/javascripts/discourse/app/components/user-tip.hbs b/app/assets/javascripts/discourse/app/components/user-tip.hbs new file mode 100644 index 0000000000..dc4b3e55d5 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-tip.hbs @@ -0,0 +1 @@ + diff --git a/app/assets/javascripts/discourse/app/components/user-tip.js b/app/assets/javascripts/discourse/app/components/user-tip.js new file mode 100644 index 0000000000..d85656f898 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-tip.js @@ -0,0 +1,35 @@ +import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; +import Component from "@glimmer/component"; +import { hideUserTip } from "discourse/lib/user-tips"; +import I18n from "I18n"; + +export default class UserTip extends Component { + @service currentUser; + + @action + showUserTip(element) { + if (!this.currentUser) { + return; + } + + const { id, selector, content, placement } = this.args; + this.currentUser.showUserTip({ + id, + + titleText: I18n.t(`user_tips.${id}.title`), + contentText: content || I18n.t(`user_tips.${id}.content`), + + reference: selector + ? element.parentElement.querySelector(selector) || element.parentElement + : element, + appendTo: element.parentElement, + + placement: placement || "top", + }); + } + + willDestroy() { + hideUserTip(this.args.id); + } +} diff --git a/app/assets/javascripts/discourse/app/controllers/application.js b/app/assets/javascripts/discourse/app/controllers/application.js index 1f4c3c7cd5..8e5a28645d 100644 --- a/app/assets/javascripts/discourse/app/controllers/application.js +++ b/app/assets/javascripts/discourse/app/controllers/application.js @@ -20,7 +20,9 @@ export default Controller.extend({ init() { this._super(...arguments); this.showSidebar = - this.canDisplaySidebar && !this.keyValueStore.getItem(HIDE_SIDEBAR_KEY); + this.canDisplaySidebar && + !this.keyValueStore.getItem(HIDE_SIDEBAR_KEY) && + !this.site.narrowDesktopView; }, @discourseComputed diff --git a/app/assets/javascripts/discourse/app/controllers/avatar-selector.js b/app/assets/javascripts/discourse/app/controllers/avatar-selector.js index d63f464c2a..eab7d45a2a 100644 --- a/app/assets/javascripts/discourse/app/controllers/avatar-selector.js +++ b/app/assets/javascripts/discourse/app/controllers/avatar-selector.js @@ -6,6 +6,7 @@ import { allowsImages } from "discourse/lib/uploads"; import discourseComputed from "discourse-common/utils/decorators"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { setting } from "discourse/lib/computed"; +import { isTesting } from "discourse-common/config/environment"; export default Controller.extend(ModalFunctionality, { gravatarName: setting("gravatar_name"), @@ -175,7 +176,11 @@ export default Controller.extend(ModalFunctionality, { this.user .pickAvatar(selectedUploadId, type) - .then(() => window.location.reload()) + .then(() => { + if (!isTesting()) { + window.location.reload(); + } + }) .catch(popupAjaxError); }, }, diff --git a/app/assets/javascripts/discourse/app/controllers/composer.js b/app/assets/javascripts/discourse/app/controllers/composer.js index a25874f18d..f56dab1204 100644 --- a/app/assets/javascripts/discourse/app/controllers/composer.js +++ b/app/assets/javascripts/discourse/app/controllers/composer.js @@ -67,6 +67,7 @@ async function loadDraft(store, opts = {}) { } const _popupMenuOptionsCallbacks = []; +const _composerSaveErrorCallbacks = []; let _checkDraftPopup = !isTesting(); @@ -82,6 +83,14 @@ export function addPopupMenuOptionsCallback(callback) { _popupMenuOptionsCallbacks.push(callback); } +export function clearComposerSaveErrorCallback() { + _composerSaveErrorCallbacks.length = 0; +} + +export function addComposerSaveErrorCallback(callback) { + _composerSaveErrorCallbacks.push(callback); +} + export default Controller.extend({ topicController: controller("topic"), router: service(), @@ -1039,9 +1048,20 @@ export default Controller.extend({ .catch((error) => { composer.set("disableDrafts", false); if (error) { - this.appEvents.one("composer:will-open", () => - this.dialog.alert(error) - ); + this.appEvents.one("composer:will-open", () => { + if ( + _composerSaveErrorCallbacks.length === 0 || + !_composerSaveErrorCallbacks + .map((c) => { + return c.call(this, error); + }) + .some((i) => { + return i; + }) + ) { + this.dialog.alert(error); + } + }); } }); @@ -1234,7 +1254,9 @@ export default Controller.extend({ if (!this.model.targetRecipients) { if (opts.usernames) { - deprecated("`usernames` is deprecated, use `recipients` instead."); + deprecated("`usernames` is deprecated, use `recipients` instead.", { + id: "discourse.composer.usernames", + }); this.model.set("targetRecipients", opts.usernames); } else if (opts.recipients) { this.model.set("targetRecipients", opts.recipients); diff --git a/app/assets/javascripts/discourse/app/controllers/create-account.js b/app/assets/javascripts/discourse/app/controllers/create-account.js index 370f0c210f..997a72afd5 100644 --- a/app/assets/javascripts/discourse/app/controllers/create-account.js +++ b/app/assets/javascripts/discourse/app/controllers/create-account.js @@ -116,8 +116,7 @@ export default Controller.extend( @discourseComputed fullnameRequired() { return ( - this.get("siteSettings.full_name_required") || - this.get("siteSettings.enable_names") + this.siteSettings.full_name_required || this.siteSettings.enable_names ); }, @@ -129,9 +128,9 @@ export default Controller.extend( @discourseComputed disclaimerHtml() { return I18n.t("create_account.disclaimer", { - tos_link: this.get("siteSettings.tos_url") || getURL("/tos"), + tos_link: this.siteSettings.tos_url || getURL("/tos"), privacy_link: - this.get("siteSettings.privacy_policy_url") || getURL("/privacy"), + this.siteSettings.privacy_policy_url || getURL("/privacy"), }); }, diff --git a/app/assets/javascripts/discourse/app/controllers/discovery/topics.js b/app/assets/javascripts/discourse/app/controllers/discovery/topics.js index caa92925ba..fb0be3912f 100644 --- a/app/assets/javascripts/discourse/app/controllers/discovery/topics.js +++ b/app/assets/javascripts/discourse/app/controllers/discovery/topics.js @@ -71,7 +71,11 @@ const controllerOpts = { changeSort() { deprecated( "changeSort has been changed from an (action) to a (route-action)", - { since: "2.6.0", dropFrom: "2.7.0" } + { + since: "2.6.0", + dropFrom: "2.7.0", + id: "discourse.topics.change-sort", + } ); return routeAction("changeSort", this.router._router, ...arguments)(); }, diff --git a/app/assets/javascripts/discourse/app/controllers/invites-show.js b/app/assets/javascripts/discourse/app/controllers/invites-show.js index c9ad115707..ea121fac4b 100644 --- a/app/assets/javascripts/discourse/app/controllers/invites-show.js +++ b/app/assets/javascripts/discourse/app/controllers/invites-show.js @@ -31,6 +31,9 @@ export default Controller.extend( accountEmail: alias("email"), existingUserId: readOnly("model.existing_user_id"), existingUserCanRedeem: readOnly("model.existing_user_can_redeem"), + existingUserCanRedeemError: readOnly( + "model.existing_user_can_redeem_error" + ), existingUserRedeeming: bool("existingUserId"), hiddenEmail: alias("model.hidden_email"), emailVerifiedByLink: alias("model.email_verified_by_link"), diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/tracking.js b/app/assets/javascripts/discourse/app/controllers/preferences/tracking.js new file mode 100644 index 0000000000..0900a9fc6f --- /dev/null +++ b/app/assets/javascripts/discourse/app/controllers/preferences/tracking.js @@ -0,0 +1,175 @@ +import Controller from "@ember/controller"; +import { NotificationLevels } from "discourse/lib/notification-levels"; +import I18n from "I18n"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { action, computed } from "@ember/object"; +import { inject as service } from "@ember/service"; +import { tracked } from "@glimmer/tracking"; + +export default class extends Controller { + @service currentUser; + @service siteSettings; + @tracked saved = false; + + likeNotificationFrequencies = [ + { name: I18n.t("user.like_notification_frequency.always"), value: 0 }, + { + name: I18n.t("user.like_notification_frequency.first_time_and_daily"), + value: 1, + }, + { name: I18n.t("user.like_notification_frequency.first_time"), value: 2 }, + { name: I18n.t("user.like_notification_frequency.never"), value: 3 }, + ]; + + autoTrackDurations = [ + { name: I18n.t("user.auto_track_options.never"), value: -1 }, + { name: I18n.t("user.auto_track_options.immediately"), value: 0 }, + { + name: I18n.t("user.auto_track_options.after_30_seconds"), + value: 30000, + }, + { name: I18n.t("user.auto_track_options.after_1_minute"), value: 60000 }, + { + name: I18n.t("user.auto_track_options.after_2_minutes"), + value: 120000, + }, + { + name: I18n.t("user.auto_track_options.after_3_minutes"), + value: 180000, + }, + { + name: I18n.t("user.auto_track_options.after_4_minutes"), + value: 240000, + }, + { + name: I18n.t("user.auto_track_options.after_5_minutes"), + value: 300000, + }, + { + name: I18n.t("user.auto_track_options.after_10_minutes"), + value: 600000, + }, + ]; + + notificationLevelsForReplying = [ + { + name: I18n.t("topic.notifications.watching.title"), + value: NotificationLevels.WATCHING, + }, + { + name: I18n.t("topic.notifications.tracking.title"), + value: NotificationLevels.TRACKING, + }, + { + name: I18n.t("topic.notifications.regular.title"), + value: NotificationLevels.REGULAR, + }, + ]; + + considerNewTopicOptions = [ + { name: I18n.t("user.new_topic_duration.not_viewed"), value: -1 }, + { name: I18n.t("user.new_topic_duration.after_1_day"), value: 60 * 24 }, + { name: I18n.t("user.new_topic_duration.after_2_days"), value: 60 * 48 }, + { + name: I18n.t("user.new_topic_duration.after_1_week"), + value: 7 * 60 * 24, + }, + { + name: I18n.t("user.new_topic_duration.after_2_weeks"), + value: 2 * 7 * 60 * 24, + }, + { name: I18n.t("user.new_topic_duration.last_here"), value: -2 }, + ]; + + get canSee() { + return this.currentUser.id === this.model.id; + } + + @computed( + "model.watched_tags.[]", + "model.watching_first_post_tags.[]", + "model.tracked_tags.[]", + "model.muted_tags.[]" + ) + get selectedTags() { + return [] + .concat( + this.model.watched_tags, + this.model.watching_first_post_tags, + this.model.tracked_tags, + this.model.muted_tags + ) + .filter((t) => t); + } + + @computed( + "model.watchedCategories", + "model.watchedFirstPostCategories", + "model.trackedCategories", + "model.mutedCategories", + "model.regularCategories", + "siteSettings.mute_all_categories_by_default" + ) + get selectedCategories() { + return [] + .concat( + this.model.watchedCategories, + this.model.watchedFirstPostCategories, + this.model.trackedCategories, + this.siteSettings.mute_all_categories_by_default + ? this.model.regularCategories + : this.model.mutedCategories + ) + .filter((t) => t); + } + + @computed("siteSettings.remove_muted_tags_from_latest") + get hideMutedTags() { + return this.siteSettings.remove_muted_tags_from_latest !== "never"; + } + + get canSave() { + return this.canSee || this.currentUser.admin; + } + + @computed( + "siteSettings.tagging_enabled", + "siteSettings.mute_all_categories_by_default" + ) + get saveAttrNames() { + const attrs = [ + "new_topic_duration_minutes", + "auto_track_topics_after_msecs", + "notification_level_when_replying", + this.siteSettings.mute_all_categories_by_default + ? "regular_category_ids" + : "muted_category_ids", + "watched_category_ids", + "tracked_category_ids", + "watched_first_post_category_ids", + ]; + + if (this.siteSettings.tagging_enabled) { + attrs.push( + "muted_tags", + "tracked_tags", + "watched_tags", + "watching_first_post_tags" + ); + } + + return attrs; + } + + @action + save() { + this.saved = false; + + return this.model + .save(this.saveAttrNames) + .then(() => { + this.saved = true; + }) + .catch(popupAjaxError); + } +} diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/users.js b/app/assets/javascripts/discourse/app/controllers/preferences/users.js index f47f7efec3..81eb728d3f 100644 --- a/app/assets/javascripts/discourse/app/controllers/preferences/users.js +++ b/app/assets/javascripts/discourse/app/controllers/preferences/users.js @@ -51,6 +51,7 @@ export default Controller.extend({ this._super(...arguments); this.saveAttrNames = [ + "allow_private_messages", "muted_usernames", "allowed_pm_usernames", "enable_allowed_pm_users", @@ -72,11 +73,6 @@ export default Controller.extend({ return !allowPrivateMessages; }, - @discourseComputed("currentUser.can_send_private_messages") - showMessageSettings() { - return this.currentUser?.can_send_private_messages; - }, - @action save() { this.set("saved", false); diff --git a/app/assets/javascripts/discourse/app/controllers/topic-bulk-actions.js b/app/assets/javascripts/discourse/app/controllers/topic-bulk-actions.js index 205cc147a4..cd5d2cef03 100644 --- a/app/assets/javascripts/discourse/app/controllers/topic-bulk-actions.js +++ b/app/assets/javascripts/discourse/app/controllers/topic-bulk-actions.js @@ -89,7 +89,7 @@ addBulkButton("showTagTopics", "change_tags", { class: "btn-default", enabledSetting: "tagging_enabled", buttonVisible() { - return this.currentUser.staff; + return this.currentUser.canManageTopic; }, }); addBulkButton("showAppendTagTopics", "append_tags", { @@ -97,7 +97,7 @@ addBulkButton("showAppendTagTopics", "append_tags", { class: "btn-default", enabledSetting: "tagging_enabled", buttonVisible() { - return this.currentUser.staff; + return this.currentUser.canManageTopic; }, }); addBulkButton("removeTags", "remove_tags", { @@ -105,7 +105,7 @@ addBulkButton("removeTags", "remove_tags", { class: "btn-default", enabledSetting: "tagging_enabled", buttonVisible() { - return this.currentUser.staff; + return this.currentUser.canManageTopic; }, }); addBulkButton("deleteTopics", "delete", { diff --git a/app/assets/javascripts/discourse/app/controllers/topic.js b/app/assets/javascripts/discourse/app/controllers/topic.js index 7e473e321c..eca4aca13d 100644 --- a/app/assets/javascripts/discourse/app/controllers/topic.js +++ b/app/assets/javascripts/discourse/app/controllers/topic.js @@ -611,6 +611,10 @@ export default Controller.extend(bufferedProperty("model"), { // Post related methods replyToPost(post) { + if (this.currentUser && this.siteSettings.enable_user_tips) { + this.currentUser.hideUserTipForever("post_menu"); + } + const composerController = this.composer; const topic = post ? post.get("topic") : this.model; const quoteState = this.quoteState; diff --git a/app/assets/javascripts/discourse/app/index.html b/app/assets/javascripts/discourse/app/index.html index d0b347c7d9..1014dd1de7 100644 --- a/app/assets/javascripts/discourse/app/index.html +++ b/app/assets/javascripts/discourse/app/index.html @@ -2,6 +2,12 @@ + Discourse - Ember CLI diff --git a/app/assets/javascripts/discourse/app/initializers/composer-hashtag-autocomplete.js b/app/assets/javascripts/discourse/app/initializers/composer-hashtag-autocomplete.js deleted file mode 100644 index b46fab8cd2..0000000000 --- a/app/assets/javascripts/discourse/app/initializers/composer-hashtag-autocomplete.js +++ /dev/null @@ -1,16 +0,0 @@ -import { withPluginApi } from "discourse/lib/plugin-api"; - -export default { - name: "composer-hashtag-autocomplete", - - initialize(container) { - const siteSettings = container.lookup("service:site-settings"); - - withPluginApi("1.4.0", (api) => { - if (siteSettings.enable_experimental_hashtag_autocomplete) { - api.registerHashtagSearchParam("category", "topic-composer", 100); - api.registerHashtagSearchParam("tag", "topic-composer", 50); - } - }); - }, -}; diff --git a/app/assets/javascripts/discourse/app/initializers/inject-objects.js b/app/assets/javascripts/discourse/app/initializers/inject-objects.js index 4885d3cd44..ea54bb1a7b 100644 --- a/app/assets/javascripts/discourse/app/initializers/inject-objects.js +++ b/app/assets/javascripts/discourse/app/initializers/inject-objects.js @@ -17,6 +17,7 @@ export default { { since: "2.8", dropFrom: "2.9", + id: "discourse.global.site-settings", } ); return container.lookup("service:site-settings"); @@ -29,6 +30,7 @@ export default { { since: "2.8", dropFrom: "2.9", + id: "discourse.global.user", } ); return User; @@ -41,6 +43,7 @@ export default { { since: "2.8", dropFrom: "2.9", + id: "discourse.global.site", } ); return Site; diff --git a/app/assets/javascripts/discourse/app/initializers/jquery-plugins.js b/app/assets/javascripts/discourse/app/initializers/jquery-plugins.js index 60487deb30..e9844f8dee 100644 --- a/app/assets/javascripts/discourse/app/initializers/jquery-plugins.js +++ b/app/assets/javascripts/discourse/app/initializers/jquery-plugins.js @@ -19,7 +19,9 @@ export default { deprecated( "`bootbox.alert` is deprecated, please use the dialog service instead.", { + id: "discourse.bootbox", dropFrom: "3.1.0.beta5", + url: "https://meta.discourse.org/t/244902", } ); return dialog.alert(arguments[0]); @@ -34,7 +36,9 @@ export default { deprecated( "`bootbox` is now deprecated, please use the dialog service instead.", { + id: "discourse.bootbox", dropFrom: "3.1.0.beta5", + url: "https://meta.discourse.org/t/244902", } ); return originalDialog(...arguments); diff --git a/app/assets/javascripts/discourse/app/initializers/narrow-desktop.js b/app/assets/javascripts/discourse/app/initializers/narrow-desktop.js new file mode 100644 index 0000000000..fdd5d769bd --- /dev/null +++ b/app/assets/javascripts/discourse/app/initializers/narrow-desktop.js @@ -0,0 +1,45 @@ +import NarrowDesktop from "discourse/lib/narrow-desktop"; + +export default { + name: "narrow-desktop", + + initialize(container) { + NarrowDesktop.init(); + let site; + if (!container.isDestroyed) { + site = container.lookup("service:site"); + site.set("narrowDesktopView", NarrowDesktop.narrowDesktopView); + } + + if ("ResizeObserver" in window) { + this._resizeObserver = new ResizeObserver((entries) => { + if (container.isDestroyed) { + return; + } + for (let entry of entries) { + const oldNarrowDesktopView = site.narrowDesktopView; + const newNarrowDesktopView = NarrowDesktop.isNarrowDesktopView( + entry.contentRect.width + ); + if (oldNarrowDesktopView !== newNarrowDesktopView) { + const applicationController = container.lookup( + "controller:application" + ); + site.set("narrowDesktopView", newNarrowDesktopView); + if (newNarrowDesktopView) { + applicationController.set("showSidebar", false); + } + applicationController.appEvents.trigger( + "site-header:force-refresh" + ); + } + } + }); + + const bodyElement = document.querySelector("body"); + if (bodyElement) { + this._resizeObserver.observe(bodyElement); + } + } + }, +}; diff --git a/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js b/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js index 8016097928..908c88f0db 100644 --- a/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js +++ b/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js @@ -1,4 +1,3 @@ -import { set } from "@ember/object"; // Subscribes to user events on the message bus import { alertChannel, @@ -161,8 +160,9 @@ export default { ); }); - bus.subscribe("/client_settings", (data) => - set(siteSettings, data.name, data.value) + bus.subscribe( + "/client_settings", + (data) => (siteSettings[data.name] = data.value) ); if (!isTesting()) { diff --git a/app/assets/javascripts/discourse/app/initializers/user-tips.js b/app/assets/javascripts/discourse/app/initializers/user-tips.js new file mode 100644 index 0000000000..5b4168c034 --- /dev/null +++ b/app/assets/javascripts/discourse/app/initializers/user-tips.js @@ -0,0 +1,29 @@ +export default { + name: "user-tips", + after: "message-bus", + + initialize(container) { + const currentUser = container.lookup("service:current-user"); + if (!currentUser) { + return; + } + + const messageBus = container.lookup("service:message-bus"); + const site = container.lookup("service:site"); + + messageBus.subscribe("/user-tips", function (seenUserTips) { + currentUser.set("seen_popups", seenUserTips); + if (!currentUser.user_option) { + currentUser.set("user_option", {}); + } + currentUser.set("user_option.seen_popups", seenUserTips); + (seenUserTips || []).forEach((userTipId) => { + currentUser.hideUserTipForever( + Object.keys(site.user_tips).find( + (id) => site.user_tips[id] === userTipId + ) + ); + }); + }); + }, +}; diff --git a/app/assets/javascripts/discourse/app/lib/category-hashtags.js b/app/assets/javascripts/discourse/app/lib/category-hashtags.js index b8ed69c74f..4d8b6a02ab 100644 --- a/app/assets/javascripts/discourse/app/lib/category-hashtags.js +++ b/app/assets/javascripts/discourse/app/lib/category-hashtags.js @@ -16,6 +16,7 @@ export function categoryHashtagTriggerRule(textarea, opts) { { since: "2.9.0.beta10", dropFrom: "3.0.0.beta1", + id: "discourse.category-hashtags.categoryHashtagTriggerRule", } ); return hashtagTriggerRule(textarea, opts); diff --git a/app/assets/javascripts/discourse/app/lib/click-track.js b/app/assets/javascripts/discourse/app/lib/click-track.js index fffea0068e..52b45ec9b4 100644 --- a/app/assets/javascripts/discourse/app/lib/click-track.js +++ b/app/assets/javascripts/discourse/app/lib/click-track.js @@ -5,7 +5,6 @@ import User from "discourse/models/user"; import { ajax } from "discourse/lib/ajax"; import getURL, { samePrefix } from "discourse-common/lib/get-url"; import { isTesting } from "discourse-common/config/environment"; -import discourseLater from "discourse-common/lib/later"; import { selectedText } from "discourse/lib/utilities"; import { wantsNewWindow } from "discourse/lib/intercept-click"; import deprecated from "discourse-common/lib/deprecated"; @@ -18,14 +17,15 @@ export function isValidLink(link) { deprecated("isValidLink now expects an Element, not a jQuery object", { since: "2.9.0.beta7", + id: "discourse.click-track.is-valid-link-jquery", }); } - // .hashtag == category/tag link + // .hashtag/.hashtag-cooked == category/tag link // .back == quote back ^ button if ( - ["lightbox", "no-track-link", "hashtag", "back"].some((name) => - link.classList.contains(name) + ["lightbox", "no-track-link", "hashtag", "hashtag-cooked", "back"].some( + (name) => link.classList.contains(name) ) ) { return false; @@ -46,7 +46,9 @@ export function isValidLink(link) { return ( link.classList.contains("track-link") || - !link.closest(".hashtag, .badge-category, .onebox-result, .onebox-body") + !link.closest( + ".hashtag, .hashtag-cooked, .badge-category, .onebox-result, .onebox-body" + ) ); } @@ -56,7 +58,7 @@ export function shouldOpenInNewTab(href) { return !isInternal && openExternalInNewTab; } -export function openLinkInNewTab(link) { +export function openLinkInNewTab(event, link) { let href = (link.href || link.dataset.href || "").trim(); if (href === "") { return; @@ -66,23 +68,7 @@ export function openLinkInNewTab(link) { newWindow.opener = null; newWindow.focus(); - // Hack to prevent changing current window.location. - // e.preventDefault() does not work. - if (!link.dataset.href) { - link.classList.add("no-href"); - link.dataset.href = link.href; - link.dataset.autoRoute = true; - link.removeAttribute("href"); - - discourseLater(() => { - if (link) { - link.classList.remove("no-href"); - link.setAttribute("href", link.dataset.href); - delete link.dataset.href; - delete link.dataset.autoRoute; - } - }, 50); - } + event.preventDefault(); } export default { @@ -103,7 +89,9 @@ export default { const link = e.currentTarget; const tracking = isValidLink(link); - // Return early for mentions and group mentions + // Return early for mentions and group mentions. This is not in + // isValidLink because returning true here allows the group card + // to pop up. If we returned false it would not. if ( ["mention", "mention-group"].some((name) => link.classList.contains(name)) ) { @@ -180,7 +168,7 @@ export default { if (!wantsNewWindow(e)) { if (shouldOpenInNewTab(href)) { - openLinkInNewTab(link); + openLinkInNewTab(e, link); } else { trackPromise.finally(() => { if (DiscourseURL.isInternal(href) && samePrefix(href)) { diff --git a/app/assets/javascripts/discourse/app/lib/cookie.js b/app/assets/javascripts/discourse/app/lib/cookie.js index 8409200930..43c0602e22 100644 --- a/app/assets/javascripts/discourse/app/lib/cookie.js +++ b/app/assets/javascripts/discourse/app/lib/cookie.js @@ -76,7 +76,11 @@ export function removeCookie(key, options) { } if (window && window.$) { - const depOpts = { since: "2.6.0", dropFrom: "2.7.0" }; + const depOpts = { + since: "2.6.0", + dropFrom: "2.7.0", + id: "discourse.jquery-cookie", + }; window.$.cookie = function () { deprecated( "$.cookie is being removed from Discourse. Please import our cookie module and use that instead.", diff --git a/app/assets/javascripts/discourse/app/lib/formatter.js b/app/assets/javascripts/discourse/app/lib/formatter.js index 8465df26a1..66cd999195 100644 --- a/app/assets/javascripts/discourse/app/lib/formatter.js +++ b/app/assets/javascripts/discourse/app/lib/formatter.js @@ -56,6 +56,7 @@ export function updateRelativeAge(elems) { deprecated("updateRelativeAge now expects a DOM NodeList", { since: "2.8.0.beta7", dropFrom: "2.9.0.beta1", + id: "discourse.formatter.update-relative-age-node-list", }); } diff --git a/app/assets/javascripts/discourse/app/lib/hashtag-autocomplete.js b/app/assets/javascripts/discourse/app/lib/hashtag-autocomplete.js index e62e0ea507..786486b770 100644 --- a/app/assets/javascripts/discourse/app/lib/hashtag-autocomplete.js +++ b/app/assets/javascripts/discourse/app/lib/hashtag-autocomplete.js @@ -8,32 +8,45 @@ import discourseDebounce from "discourse-common/lib/debounce"; import { caretPosition, caretRowCol, + escapeExpression, inCodeBlock, } from "discourse/lib/utilities"; import { search as searchCategoryTag } from "discourse/lib/category-tag-search"; +import { emojiUnescape } from "discourse/lib/text"; +import { htmlSafe } from "@ember/template"; +/** + * Sets up a textarea using the jQuery autocomplete plugin, specifically + * to match on the hashtag (#) character for autocompletion of categories, + * tags, and other resource data types. + * + * @param {Array} contextualHashtagConfiguration - The hashtag datasource types in priority order + * that should be used when searching for or looking up hashtags from the server, determines + * the order of search results and the priority for looking up conflicting hashtags. See also + * Site.hashtag_configurations. + * @param {$Element} $textarea - jQuery element to use for the autocompletion + * plugin to attach to, this is what will watch for the # matcher when the user is typing. + * @param {Hash} siteSettings - The clientside site settings. + * @param {Function} afterComplete - Called with the selected autocomplete option once it is selected. + **/ export function setupHashtagAutocomplete( - context, + contextualHashtagConfiguration, $textArea, siteSettings, afterComplete ) { if (siteSettings.enable_experimental_hashtag_autocomplete) { - _setupExperimental(context, $textArea, siteSettings, afterComplete); + _setupExperimental( + contextualHashtagConfiguration, + $textArea, + siteSettings, + afterComplete + ); } else { _setup($textArea, siteSettings, afterComplete); } } -const contextBasedParams = {}; - -export function registerHashtagSearchParam(param, context, priority) { - if (!contextBasedParams[context]) { - contextBasedParams[context] = {}; - } - contextBasedParams[context][param] = priority; -} - export function hashtagTriggerRule(textarea, opts) { const result = caretRowCol(textarea); const row = result.rowNum; @@ -62,7 +75,63 @@ export function hashtagTriggerRule(textarea, opts) { return true; } -function _setupExperimental(context, $textArea, siteSettings, afterComplete) { +const checkedHashtags = new Set(); +let seenHashtags = {}; + +// NOTE: For future maintainers, the hashtag lookup here does not take +// into account mixed contexts -- for instance, a chat quote inside a post +// or a post quote inside a chat message, so this may +// not provide an accurate priority lookup for hashtags without a ::type suffix in those +// cases. +export function fetchUnseenHashtagsInContext( + contextualHashtagConfiguration, + slugs +) { + return ajax("/hashtags", { + data: { slugs, order: contextualHashtagConfiguration }, + }).then((response) => { + Object.keys(response).forEach((type) => { + seenHashtags[type] = seenHashtags[type] || {}; + response[type].forEach((item) => { + seenHashtags[type][item.ref] = seenHashtags[type][item.ref] || item; + }); + }); + slugs.forEach(checkedHashtags.add, checkedHashtags); + }); +} + +export function linkSeenHashtagsInContext( + contextualHashtagConfiguration, + elem +) { + const hashtagSpans = [...(elem?.querySelectorAll("span.hashtag-raw") || [])]; + if (hashtagSpans.length === 0) { + return []; + } + const slugs = [ + ...hashtagSpans.map((span) => span.innerText.replace("#", "")), + ]; + + hashtagSpans.forEach((hashtagSpan, index) => { + _findAndReplaceSeenHashtagPlaceholder( + slugs[index], + contextualHashtagConfiguration, + hashtagSpan + ); + }); + + return slugs + .map((slug) => slug.toLowerCase()) + .uniq() + .filter((slug) => !checkedHashtags.has(slug)); +} + +function _setupExperimental( + contextualHashtagConfiguration, + $textArea, + siteSettings, + afterComplete +) { $textArea.autocomplete({ template: findRawTemplate("hashtag-autocomplete"), key: "#", @@ -73,7 +142,7 @@ function _setupExperimental(context, $textArea, siteSettings, afterComplete) { if (term.match(/\s/)) { return null; } - return _searchGeneric(term, siteSettings, context); + return _searchGeneric(term, siteSettings, contextualHashtagConfiguration); }, triggerRule: (textarea, opts) => hashtagTriggerRule(textarea, opts), }); @@ -105,7 +174,7 @@ function _updateSearchCache(term, results) { return results; } -function _searchGeneric(term, siteSettings, context) { +function _searchGeneric(term, siteSettings, contextualHashtagConfiguration) { if (currentSearch) { currentSearch.abort(); currentSearch = null; @@ -133,19 +202,23 @@ function _searchGeneric(term, siteSettings, context) { discourseDebounce(this, _searchRequest, q, ctx, resultFunc, INPUT_DELAY); }; - debouncedSearch(term, context, (result) => { + debouncedSearch(term, contextualHashtagConfiguration, (result) => { cancel(timeoutPromise); resolve(_updateSearchCache(term, result)); }); }); } -function _searchRequest(term, context, resultFunc) { +function _searchRequest(term, contextualHashtagConfiguration, resultFunc) { currentSearch = ajax("/hashtags/search.json", { - data: { term, order: _sortedContextParams(context) }, + data: { term, order: contextualHashtagConfiguration }, }); currentSearch .then((r) => { + r.results?.forEach((result) => { + // Convert :emoji: in the result text to HTML safely. + result.text = htmlSafe(emojiUnescape(escapeExpression(result.text))); + }); resultFunc(r.results || CANCELLED_STATUS); }) .finally(() => { @@ -154,8 +227,30 @@ function _searchRequest(term, context, resultFunc) { return currentSearch; } -function _sortedContextParams(context) { - return Object.entries(contextBasedParams[context]) - .sort((a, b) => b[1] - a[1]) - .map((item) => item[0]); +function _findAndReplaceSeenHashtagPlaceholder( + slug, + contextualHashtagConfiguration, + hashtagSpan +) { + contextualHashtagConfiguration.forEach((type) => { + // remove type suffixes + const typePostfix = `::${type}`; + if (slug.endsWith(typePostfix)) { + slug = slug.slice(0, slug.length - typePostfix.length); + } + + // replace raw span for the hashtag with a cooked one + const matchingSeenHashtag = seenHashtags[type]?.[slug]; + if (matchingSeenHashtag) { + // NOTE: When changing the HTML structure here, you must also change + // it in the hashtag-autocomplete markdown rule, and vice-versa. + const link = document.createElement("a"); + link.classList.add("hashtag-cooked"); + link.href = matchingSeenHashtag.relative_url; + link.dataset.type = type; + link.dataset.slug = matchingSeenHashtag.slug; + link.innerHTML = `${matchingSeenHashtag.text}`; + hashtagSpan.replaceWith(link); + } + }); } diff --git a/app/assets/javascripts/discourse/app/lib/link-hashtags.js b/app/assets/javascripts/discourse/app/lib/link-hashtags.js index 5dee24413c..0138379fec 100644 --- a/app/assets/javascripts/discourse/app/lib/link-hashtags.js +++ b/app/assets/javascripts/discourse/app/lib/link-hashtags.js @@ -15,6 +15,7 @@ export function linkSeenHashtags(elem) { deprecated("linkSeenHashtags now expects a DOM node as first parameter", { since: "2.8.0.beta7", dropFrom: "2.9.0.beta1", + id: "discourse.link-hashtags.dom-node", }); } diff --git a/app/assets/javascripts/discourse/app/lib/link-mentions.js b/app/assets/javascripts/discourse/app/lib/link-mentions.js index 0ce78017c6..0af0ef6b44 100644 --- a/app/assets/javascripts/discourse/app/lib/link-mentions.js +++ b/app/assets/javascripts/discourse/app/lib/link-mentions.js @@ -74,6 +74,7 @@ export function linkSeenMentions(elem, siteSettings) { deprecated("linkSeenMentions now expects a DOM node as first parameter", { since: "2.8.0.beta7", dropFrom: "2.9.0.beta1", + id: "discourse.link-mentions.dom-node", }); } diff --git a/app/assets/javascripts/discourse/app/lib/narrow-desktop.js b/app/assets/javascripts/discourse/app/lib/narrow-desktop.js new file mode 100644 index 0000000000..96c56f82c3 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/narrow-desktop.js @@ -0,0 +1,16 @@ +let narrowDesktopForced = false; + +const NarrowDesktop = { + narrowDesktopView: false, + + init() { + this.narrowDesktopView = + narrowDesktopForced || this.isNarrowDesktopView(window.innerWidth); + }, + + isNarrowDesktopView(width) { + return width < 1100; + }, +}; + +export default NarrowDesktop; diff --git a/app/assets/javascripts/discourse/app/lib/offset-calculator.js b/app/assets/javascripts/discourse/app/lib/offset-calculator.js index bd1f8556d2..438bb5a5ff 100644 --- a/app/assets/javascripts/discourse/app/lib/offset-calculator.js +++ b/app/assets/javascripts/discourse/app/lib/offset-calculator.js @@ -10,6 +10,7 @@ export function minimumOffset() { { since: "2.8.0.beta10", dropFrom: "2.9.0.beta2", + id: "discourse.offset-calculator.minimumOffset", } ); diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index fc20f67150..b55155b5b8 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -55,7 +55,10 @@ import { addNavItem } from "discourse/models/nav-item"; import { addPluginDocumentTitleCounter } from "discourse/components/d-document"; import { addPluginOutletDecorator } from "discourse/components/plugin-connector"; import { addPluginReviewableParam } from "discourse/components/reviewable-item"; -import { addPopupMenuOptionsCallback } from "discourse/controllers/composer"; +import { + addComposerSaveErrorCallback, + addPopupMenuOptionsCallback, +} from "discourse/controllers/composer"; import { addPostClassesCallback } from "discourse/widgets/post"; import { addGroupPostSmallActionCode, @@ -104,13 +107,12 @@ import DiscourseURL from "discourse/lib/url"; import { registerNotificationTypeRenderer } from "discourse/lib/notification-types-manager"; import { registerUserMenuTab } from "discourse/lib/user-menu/tab"; import { registerModelTransformer } from "discourse/lib/model-transformers"; -import { registerHashtagSearchParam } from "discourse/lib/hashtag-autocomplete"; // If you add any methods to the API ensure you bump up the version number // based on Semantic Versioning 2.0.0. Please update the changelog at // docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version // using the format described at https://keepachangelog.com/en/1.0.0/. -const PLUGIN_API_VERSION = "1.4.0"; +const PLUGIN_API_VERSION = "1.5.0"; // This helper prevents us from applying the same `modifyClass` over and over in test mode. function canModify(klass, type, resolverName, changes) { @@ -531,7 +533,8 @@ class PluginApi { this.addCommunitySectionLink(args, name.match(/footerLinks/)); } catch { deprecated( - `Usage of \`api.decorateWidget('hamburger-menu:generalLinks')\` is incompatible with the \`enable_experimental_sidebar_hamburger\` site setting. Please use \`api.addCommunitySectionLink\` instead.` + `Usage of \`api.decorateWidget('hamburger-menu:generalLinks')\` is incompatible with the \`enable_experimental_sidebar_hamburger\` site setting. Please use \`api.addCommunitySectionLink\` instead.`, + { id: "discourse.decorate-widget.hamburger-widget-links" } ); } @@ -798,7 +801,8 @@ class PluginApi { addFlagProperty() { deprecated( - "addFlagProperty has been removed. Use the reviewable API instead." + "addFlagProperty has been removed. Use the reviewable API instead.", + { id: "discourse.add-flag-property" } ); } @@ -1245,6 +1249,27 @@ class PluginApi { Composer.reopen({ beforeSave: method }); } + /** + * Registers a callback function to handle the composer save errors. + * This allows you to implement custom logic that will happen before + * the raw error is presented to the user. + * The passed function is expected to return true if the error was handled, + * false otherwise. + * + * Example: + * + * api.addComposerSaveErrorCallback((error) => { + * if (error == "my_error") { + * //handle error + * return true; + * } + * return false; + * }) + */ + addComposerSaveErrorCallback(callback) { + addComposerSaveErrorCallback(callback); + } + /** * Adds a field to topic edit serializer * @@ -1993,35 +2018,6 @@ class PluginApi { registerModelTransformer(modelName, transformer) { registerModelTransformer(modelName, transformer); } - - /** - * EXPERIMENTAL. Do not use. - * - * When initiating a search inside the composer or other designated inputs - * with the `#` key, we search records based on params registered with - * this function, and order them by type using the priority here. Since - * there can be many different inputs that use `#` and some may need to - * weight different types higher in priority, we also require a context - * parameter. - * - * For example, the topic composer may wish to search for categories - * and tags, with categories appearing first in the results. The usage - * looks like this: - * - * api.registerHashtagSearchParam("category", "topic-composer", 100); - * api.registerHashtagSearchParam("tag", "topic-composer", 50); - * - * Additional types of records used for the hashtag search results - * can be registered via the #register_hashtag_data_source plugin API - * method. - * - * @param {string} param - The type of record to be fetched. - * @param {string} context - Where the hashtag search is being initiated using `#` - * @param {number} priority - Used for ordering types of records. Priority order is descending. - */ - registerHashtagSearchParam(param, context, priority) { - registerHashtagSearchParam(param, context, priority); - } } // from http://stackoverflow.com/questions/6832596/how-to-compare-software-version-number-using-js-only-number diff --git a/app/assets/javascripts/discourse/app/lib/plugin-connectors.js b/app/assets/javascripts/discourse/app/lib/plugin-connectors.js index 16ff7a63a6..a276c1fe1b 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-connectors.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-connectors.js @@ -109,7 +109,9 @@ export function buildArgsWithDeprecations(args, deprecatedArgs) { Object.keys(deprecatedArgs).forEach((key) => { Object.defineProperty(output, key, { get() { - deprecated(`${key} is deprecated`); + deprecated(`${key} is deprecated`, { + id: "discourse.plugin-connector.deprecated-arg", + }); return deprecatedArgs[key]; }, diff --git a/app/assets/javascripts/discourse/app/lib/public-js-versions.js b/app/assets/javascripts/discourse/app/lib/public-js-versions.js index a8ced62efa..f807c36c3f 100644 --- a/app/assets/javascripts/discourse/app/lib/public-js-versions.js +++ b/app/assets/javascripts/discourse/app/lib/public-js-versions.js @@ -10,5 +10,5 @@ export const PUBLIC_JS_VERSIONS = { "diffhtml.min.js": "diffhtml/1.0.0-beta.20/diffhtml.min.js", "jquery.magnific-popup.min.js": "magnific-popup/1.1.0/jquery.magnific-popup.min.js", - "pikaday.js": "pikaday/1.8.0/pikaday.js", + "pikaday.js": "pikaday/1.8.2/pikaday.js", }; diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/my-posts-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/my-posts-section-link.js index 01544b54d9..82fceb4bd0 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/my-posts-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/my-posts-section-link.js @@ -47,7 +47,11 @@ export default class MyPostsSectionLink extends BaseSectionLink { } get title() { - return I18n.t("sidebar.sections.community.links.my_posts.title"); + if (this._hasDraft) { + return I18n.t("sidebar.sections.community.links.my_posts.title_drafts"); + } else { + return I18n.t("sidebar.sections.community.links.my_posts.title"); + } } get text() { diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/tracked-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/tracked-section-link.js deleted file mode 100644 index 8eaeb47129..0000000000 --- a/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/tracked-section-link.js +++ /dev/null @@ -1,101 +0,0 @@ -import I18n from "I18n"; - -import { tracked } from "@glimmer/tracking"; -import BaseSectionLink from "discourse/lib/sidebar/base-community-section-link"; -import { isTrackedTopic } from "discourse/lib/topic-list-tracked-filter"; -import { UNREAD_LIST_DESTINATION } from "discourse/controllers/preferences/sidebar"; - -export default class TrackedSectionLink extends BaseSectionLink { - @tracked totalUnread = 0; - @tracked totalNew = 0; - @tracked hideCount = - this.currentUser?.sidebarListDestination !== UNREAD_LIST_DESTINATION; - - constructor() { - super(...arguments); - this.#refreshCounts(); - } - - onTopicTrackingStateChange() { - this.#refreshCounts(); - } - - #refreshCounts() { - this.totalUnread = this.topicTrackingState.countUnread({ - customFilterFn: isTrackedTopic, - }); - - if (this.totalUnread === 0) { - this.totalNew = this.topicTrackingState.countNew({ - customFilterFn: isTrackedTopic, - }); - } - } - - get name() { - return "tracked"; - } - - get query() { - return { f: "tracked" }; - } - - get title() { - return I18n.t("sidebar.sections.community.links.tracked.title"); - } - - get text() { - return I18n.t("sidebar.sections.community.links.tracked.content"); - } - - get currentWhen() { - return "discovery.latest discovery.new discovery.unread discovery.top"; - } - - get badgeText() { - if (this.hideCount) { - return; - } - if (this.totalUnread > 0) { - return I18n.t("sidebar.unread_count", { - count: this.totalUnread, - }); - } else if (this.totalNew > 0) { - return I18n.t("sidebar.new_count", { - count: this.totalNew, - }); - } else { - return; - } - } - - get route() { - if (this.currentUser?.sidebarListDestination === UNREAD_LIST_DESTINATION) { - if (this.totalUnread > 0) { - return "discovery.unread"; - } - if (this.totalNew > 0) { - return "discovery.new"; - } - } - return "discovery.latest"; - } - - get prefixValue() { - return "bell"; - } - - get suffixCSSClass() { - return "unread"; - } - - get suffixType() { - return "icon"; - } - - get suffixValue() { - if (this.hideCount && (this.totalUnread || this.totalNew)) { - return "circle"; - } - } -} diff --git a/app/assets/javascripts/discourse/app/lib/theme-selector.js b/app/assets/javascripts/discourse/app/lib/theme-selector.js index 58d4d41fdc..a4d56442dc 100644 --- a/app/assets/javascripts/discourse/app/lib/theme-selector.js +++ b/app/assets/javascripts/discourse/app/lib/theme-selector.js @@ -11,7 +11,8 @@ export function currentThemeKey() { if (console && console.warn && console.trace) { // TODO: Remove this code Jan 2019 deprecated( - "'currentThemeKey' is is deprecated use 'currentThemeId' instead. A theme component may require updating." + "'currentThemeKey' is is deprecated use 'currentThemeId' instead. A theme component may require updating.", + { id: "discourse.current-theme-key" } ); } } diff --git a/app/assets/javascripts/discourse/app/lib/to-markdown.js b/app/assets/javascripts/discourse/app/lib/to-markdown.js index ccf6e30920..69be29cfa7 100644 --- a/app/assets/javascripts/discourse/app/lib/to-markdown.js +++ b/app/assets/javascripts/discourse/app/lib/to-markdown.js @@ -315,6 +315,18 @@ export class Tag { return text; } + if (attr.class?.includes("hashtag-cooked")) { + if (attr["data-ref"]) { + return `#${attr["data-ref"]}`; + } else { + let type = ""; + if (attr["data-type"]) { + type = `::${attr["data-type"]}`; + } + return `#${attr["data-slug"]}${type}`; + } + } + let img; if ( ["lightbox", "d-lazyload"].includes(attr.class) && diff --git a/app/assets/javascripts/discourse/app/lib/uploads.js b/app/assets/javascripts/discourse/app/lib/uploads.js index 02e0a2b673..1ea697dcc1 100644 --- a/app/assets/javascripts/discourse/app/lib/uploads.js +++ b/app/assets/javascripts/discourse/app/lib/uploads.js @@ -296,7 +296,8 @@ export function getUploadMarkdown(upload) { export function displayErrorForUpload(data, siteSettings, fileName) { if (!fileName) { deprecated( - "Calling displayErrorForUpload without a fileName is deprecated and will be removed in a future version." + "Calling displayErrorForUpload without a fileName is deprecated and will be removed in a future version.", + { id: "discourse.uploads.display-error-for-upload" } ); fileName = data.files[0].name; } diff --git a/app/assets/javascripts/discourse/app/lib/url.js b/app/assets/javascripts/discourse/app/lib/url.js index 622911c417..0bd9a28efb 100644 --- a/app/assets/javascripts/discourse/app/lib/url.js +++ b/app/assets/javascripts/discourse/app/lib/url.js @@ -196,7 +196,7 @@ const DiscourseURL = EmberObject.extend({ return; } - if (Session.currentProp("requiresRefresh")) { + if (Session.currentProp("requiresRefresh") && !this.isComposerOpen) { return this.redirectTo(path); } @@ -409,6 +409,10 @@ const DiscourseURL = EmberObject.extend({ return window.location.origin + (prefix === "/" ? "" : prefix); }, + get isComposerOpen() { + return this.controllerFor("composer")?.visible; + }, + get router() { return this.container.lookup("router:main"); }, diff --git a/app/assets/javascripts/discourse/app/lib/user-tips.js b/app/assets/javascripts/discourse/app/lib/user-tips.js index 3eefac3bde..df27654a90 100644 --- a/app/assets/javascripts/discourse/app/lib/user-tips.js +++ b/app/assets/javascripts/discourse/app/lib/user-tips.js @@ -30,6 +30,7 @@ export function showUserTip(options) { arrow: iconHTML("tippy-rounded-arrow"), placement: options.placement, + appendTo: options.appendTo, // It often happens for the reference element to be rerendered. In this // case, tippy must be rerendered too. Having an animation means that the @@ -77,6 +78,15 @@ export function hideUserTip(userTipId) { instance.destroy(); } delete instances[userTipId]; + + const index = queue.findIndex((userTip) => userTip.id === userTipId); + if (index > -1) { + queue.splice(index, 1); + } +} + +export function hideAllUserTips() { + Object.keys(instances).forEach(hideUserTip); } function addToQueue(options) { diff --git a/app/assets/javascripts/discourse/app/lib/utilities.js b/app/assets/javascripts/discourse/app/lib/utilities.js index 6af92d705a..a0096b0ee0 100644 --- a/app/assets/javascripts/discourse/app/lib/utilities.js +++ b/app/assets/javascripts/discourse/app/lib/utilities.js @@ -335,6 +335,7 @@ export function safariHacksDisabled() { { since: "2.8.0.beta8", dropFrom: "2.9.0.beta1", + id: "discourse.safari-hacks-disabled", } ); diff --git a/app/assets/javascripts/discourse/app/mixins/bulk-topic-selection.js b/app/assets/javascripts/discourse/app/mixins/bulk-topic-selection.js index 4703993b33..fe1980ed82 100644 --- a/app/assets/javascripts/discourse/app/mixins/bulk-topic-selection.js +++ b/app/assets/javascripts/discourse/app/mixins/bulk-topic-selection.js @@ -13,7 +13,11 @@ export default Mixin.create({ selected: null, lastChecked: null, - canBulkSelect: or("currentUser.staff", "showDismissRead", "showResetNew"), + canBulkSelect: or( + "currentUser.canManageTopic", + "showDismissRead", + "showResetNew" + ), @on("init") resetSelected() { diff --git a/app/assets/javascripts/discourse/app/models/composer.js b/app/assets/javascripts/discourse/app/models/composer.js index e4058e78d1..267c2412d4 100644 --- a/app/assets/javascripts/discourse/app/models/composer.js +++ b/app/assets/javascripts/discourse/app/models/composer.js @@ -143,10 +143,15 @@ const Composer = RestModel.extend({ const oldCategoryId = this._categoryId; if (isEmpty(categoryId)) { - // Set General as the default category - const generalCategoryId = this.siteSettings.general_category_id; + // Check if there is a default composer category to set + const defaultComposerCategoryId = parseInt( + this.siteSettings.default_composer_category, + 10 + ); categoryId = - generalCategoryId && generalCategoryId > 0 ? generalCategoryId : null; + defaultComposerCategoryId && defaultComposerCategoryId > 0 + ? defaultComposerCategoryId + : null; } this._categoryId = categoryId; @@ -772,7 +777,9 @@ const Composer = RestModel.extend({ } if (opts.usernames) { - deprecated("`usernames` is deprecated, use `recipients` instead."); + deprecated("`usernames` is deprecated, use `recipients` instead.", { + id: "discourse.composer.usernames", + }); } this.setProperties({ diff --git a/app/assets/javascripts/discourse/app/models/nav-item.js b/app/assets/javascripts/discourse/app/models/nav-item.js index c93a673ef4..bfc3260832 100644 --- a/app/assets/javascripts/discourse/app/models/nav-item.js +++ b/app/assets/javascripts/discourse/app/models/nav-item.js @@ -243,6 +243,7 @@ NavItem.reopenClass({ deprecated("You must supply `buildList` with a `siteSettings` object", { since: "2.6.0", dropFrom: "2.7.0", + id: "discourse.nav-item.built-list-site-settings", }); args.siteSettings = getOwner(this).lookup("service:site-settings"); } diff --git a/app/assets/javascripts/discourse/app/models/post-stream.js b/app/assets/javascripts/discourse/app/models/post-stream.js index 6cc36267a0..674ea63cf3 100644 --- a/app/assets/javascripts/discourse/app/models/post-stream.js +++ b/app/assets/javascripts/discourse/app/models/post-stream.js @@ -697,7 +697,10 @@ export default RestModel.extend({ * */ triggerNewPostInStream(postId, opts) { deprecated( - "Please use triggerNewPostsInStream, this method will be removed July 2021" + "Please use triggerNewPostsInStream, this method will be removed July 2021", + { + id: "discourse.post-stream.trigger-new-post", + } ); return this.triggerNewPostsInStream([postId], opts); }, diff --git a/app/assets/javascripts/discourse/app/models/site.js b/app/assets/javascripts/discourse/app/models/site.js index 6b1f1619e5..03e6ca0c1a 100644 --- a/app/assets/javascripts/discourse/app/models/site.js +++ b/app/assets/javascripts/discourse/app/models/site.js @@ -229,6 +229,7 @@ if (typeof Discourse !== "undefined") { if (!warned) { deprecated("Import the Site class instead of using Discourse.Site", { since: "2.4.0", + id: "discourse.globals.site", }); warned = true; } diff --git a/app/assets/javascripts/discourse/app/models/store.js b/app/assets/javascripts/discourse/app/models/store.js index 42523e3e76..cb928def5e 100644 --- a/app/assets/javascripts/discourse/app/models/store.js +++ b/app/assets/javascripts/discourse/app/models/store.js @@ -6,5 +6,6 @@ deprecated( { since: "2.8.0.beta8", dropFrom: "2.9.0.beta1", + id: "discourse.models-store", } ); diff --git a/app/assets/javascripts/discourse/app/models/topic.js b/app/assets/javascripts/discourse/app/models/topic.js index 32aa5beceb..fbccc568e1 100644 --- a/app/assets/javascripts/discourse/app/models/topic.js +++ b/app/assets/javascripts/discourse/app/models/topic.js @@ -240,13 +240,18 @@ const Topic = RestModel.extend({ @discourseComputed("unread_posts", "new_posts") totalUnread(unreadPosts, newPosts) { - deprecated("The totalUnread property of the topic model is deprecated"); + deprecated("The totalUnread property of the topic model is deprecated", { + id: "discourse.topic.totalUnread", + }); return unreadPosts || newPosts; }, @discourseComputed("unread_posts", "new_posts") displayNewPosts(unreadPosts, newPosts) { - deprecated("The displayNewPosts property of the topic model is deprecated"); + deprecated( + "The displayNewPosts property of the topic model is deprecated", + { id: "discourse.topic.totalUnread" } + ); return unreadPosts || newPosts; }, diff --git a/app/assets/javascripts/discourse/app/models/user.js b/app/assets/javascripts/discourse/app/models/user.js index 89ae1f2fbd..1a35abf31c 100644 --- a/app/assets/javascripts/discourse/app/models/user.js +++ b/app/assets/javascripts/discourse/app/models/user.js @@ -44,10 +44,12 @@ import { cancel } from "@ember/runloop"; import discourseLater from "discourse-common/lib/later"; import { isTesting } from "discourse-common/config/environment"; import { + hideAllUserTips, hideUserTip, showNextUserTip, showUserTip, } from "discourse/lib/user-tips"; +import { dependentKeyCompat } from "@ember/object/compat"; export const SECOND_FACTOR_METHODS = { TOTP: 1, @@ -445,16 +447,21 @@ const User = RestModel.extend({ type: "PUT", }) .then((result) => { - this.set("bio_excerpt", result.user.bio_excerpt); - const userProps = getProperties( - this.user_option, - "enable_quoting", - "enable_defer", - "external_links_in_new_tab", - "dynamic_favicon" - ); - User.current()?.setProperties(userProps); this.setProperties(updatedState); + this.setProperties(getProperties(result.user, "bio_excerpt")); + if (User.current() === this && result.user.user_option) { + this.setProperties( + getProperties( + result.user.user_option, + "enable_quoting", + "enable_defer", + "external_links_in_new_tab", + "dynamic_favicon", + "seen_popups", + "skip_new_user_tips" + ) + ); + } return result; }) .finally(() => { @@ -802,29 +809,59 @@ const User = RestModel.extend({ }); }, - @discourseComputed("muted_category_ids") - mutedCategories(mutedCategoryIds) { - return Category.findByIds(mutedCategoryIds); + @dependentKeyCompat + get mutedCategories() { + return Category.findByIds(this.get("muted_category_ids")); + }, + set mutedCategories(categories) { + this.set( + "muted_category_ids", + categories.map((c) => c.id) + ); }, - @discourseComputed("regular_category_ids") - regularCategories(regularCategoryIds) { - return Category.findByIds(regularCategoryIds); + @dependentKeyCompat + get regularCategories() { + return Category.findByIds(this.get("regular_category_ids")); + }, + set regularCategories(categories) { + this.set( + "regular_category_ids", + categories.map((c) => c.id) + ); }, - @discourseComputed("tracked_category_ids") - trackedCategories(trackedCategoryIds) { - return Category.findByIds(trackedCategoryIds); + @dependentKeyCompat + get trackedCategories() { + return Category.findByIds(this.get("tracked_category_ids")); + }, + set trackedCategories(categories) { + this.set( + "tracked_category_ids", + categories.map((c) => c.id) + ); }, - @discourseComputed("watched_category_ids") - watchedCategories(watchedCategoryIds) { - return Category.findByIds(watchedCategoryIds); + @dependentKeyCompat + get watchedCategories() { + return Category.findByIds(this.get("watched_category_ids")); + }, + set watchedCategories(categories) { + this.set( + "watched_category_ids", + categories.map((c) => c.id) + ); }, - @discourseComputed("watched_first_post_category_ids") - watchedFirstPostCategories(watchedFirstPostCategoryIds) { - return Category.findByIds(watchedFirstPostCategoryIds); + @dependentKeyCompat + get watchedFirstPostCategories() { + return Category.findByIds(this.get("watched_first_post_category_ids")); + }, + set watchedFirstPostCategories(categories) { + this.set( + "watched_first_post_category_ids", + categories.map((c) => c.id) + ); }, @discourseComputed("can_delete_account") @@ -1101,8 +1138,10 @@ const User = RestModel.extend({ } if (!userTips[options.id]) { - // eslint-disable-next-line no-console - console.warn("Cannot show user tip with type =", options.id); + if (!isTesting()) { + // eslint-disable-next-line no-console + console.warn("Cannot show user tip with type =", options.id); + } return; } @@ -1134,20 +1173,27 @@ const User = RestModel.extend({ return; } - // Hide any shown user tips. - let seenUserTips = this.seen_popups || []; + // Hide user tips and maybe show the next one. if (userTipId) { hideUserTip(userTipId); - if (!seenUserTips.includes(userTips[userTipId])) { - seenUserTips.push(userTips[userTipId]); - } + showNextUserTip(); } else { - Object.keys(userTips).forEach(hideUserTip); - seenUserTips = [-1]; + hideAllUserTips(); } - // Show next user tip in queue. - showNextUserTip(); + // Update list of seen user tips. + let seenUserTips = this.seen_popups || []; + if (userTipId) { + if (seenUserTips.includes(userTips[userTipId])) { + return; + } + seenUserTips.push(userTips[userTipId]); + } else { + if (seenUserTips.includes(-1)) { + return; + } + seenUserTips = [-1]; + } // Save seen user tips on the server. if (!this.user_option) { @@ -1392,6 +1438,7 @@ if (typeof Discourse !== "undefined") { if (!warned) { deprecated("Import the User class instead of using Discourse.User", { since: "2.4.0", + id: "discourse.globals.user", }); warned = true; } diff --git a/app/assets/javascripts/discourse/app/routes/app-route-map.js b/app/assets/javascripts/discourse/app/routes/app-route-map.js index fd06c0b178..452125109b 100644 --- a/app/assets/javascripts/discourse/app/routes/app-route-map.js +++ b/app/assets/javascripts/discourse/app/routes/app-route-map.js @@ -161,6 +161,7 @@ export default function () { this.route("profile"); this.route("emails"); this.route("notifications"); + this.route("tracking"); this.route("categories"); this.route("users"); this.route("tags"); diff --git a/app/assets/javascripts/discourse/app/routes/preferences-tracking.js b/app/assets/javascripts/discourse/app/routes/preferences-tracking.js new file mode 100644 index 0000000000..bfec0d17c4 --- /dev/null +++ b/app/assets/javascripts/discourse/app/routes/preferences-tracking.js @@ -0,0 +1,5 @@ +import RestrictedUserRoute from "discourse/routes/restricted-user"; + +export default RestrictedUserRoute.extend({ + showFooter: true, +}); diff --git a/app/assets/javascripts/discourse/app/services/site-settings.js b/app/assets/javascripts/discourse/app/services/site-settings.js index f550943d0a..e57c90719c 100644 --- a/app/assets/javascripts/discourse/app/services/site-settings.js +++ b/app/assets/javascripts/discourse/app/services/site-settings.js @@ -1,9 +1,10 @@ import PreloadStore from "discourse/lib/preload-store"; +import { TrackedObject } from "@ember-compat/tracked-built-ins"; export default class SiteSettingsService { static isServiceFactory = true; static create() { - return PreloadStore.get("siteSettings"); + return new TrackedObject(PreloadStore.get("siteSettings")); } } diff --git a/app/assets/javascripts/discourse/app/templates/about.hbs b/app/assets/javascripts/discourse/app/templates/about.hbs index 568edf2169..444b373c40 100644 --- a/app/assets/javascripts/discourse/app/templates/about.hbs +++ b/app/assets/javascripts/discourse/app/templates/about.hbs @@ -120,7 +120,7 @@ {{#if this.contactInfo}}
    -

    {{d-icon "far-envelope"}} {{i18n "about.contact"}}

    +

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

    {{html-safe this.contactInfo}}

    {{/if}} diff --git a/app/assets/javascripts/discourse/app/templates/application.hbs b/app/assets/javascripts/discourse/app/templates/application.hbs index 1bcc83c735..9c462727a6 100644 --- a/app/assets/javascripts/discourse/app/templates/application.hbs +++ b/app/assets/javascripts/discourse/app/templates/application.hbs @@ -13,6 +13,7 @@ @toggleAnonymous={{route-action "toggleAnonymous"}} @logout={{route-action "logout"}} @sidebarEnabled={{this.sidebarEnabled}} + @showSidebar={{this.showSidebar}} @toggleSidebar={{action "toggleSidebar"}} /> {{/if}} diff --git a/app/assets/javascripts/discourse/app/templates/components/d-button.hbs b/app/assets/javascripts/discourse/app/templates/components/d-button.hbs index a05a03a390..bb60dd2a3a 100644 --- a/app/assets/javascripts/discourse/app/templates/components/d-button.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/d-button.hbs @@ -1,16 +1,36 @@ -{{#if this.isLoading}} - {{~d-icon "spinner" class="loading-icon"~}} -{{else}} - {{#if this.icon}} - {{~d-icon this.icon~}} +{{! template-lint-disable no-down-event-binding }} + diff --git a/app/assets/javascripts/discourse/app/templates/components/edit-category-settings.hbs b/app/assets/javascripts/discourse/app/templates/components/edit-category-settings.hbs index 2f9f51a47f..5df8c8dba4 100644 --- a/app/assets/javascripts/discourse/app/templates/components/edit-category-settings.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/edit-category-settings.hbs @@ -229,7 +229,7 @@ {{#if this.emailInEnabled}}
    diff --git a/app/assets/javascripts/discourse/app/templates/components/reviewable-queued-post.hbs b/app/assets/javascripts/discourse/app/templates/components/reviewable-queued-post.hbs index 39b8e7dd3a..ff7e70e3af 100644 --- a/app/assets/javascripts/discourse/app/templates/components/reviewable-queued-post.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/reviewable-queued-post.hbs @@ -7,7 +7,7 @@ {{#if this.reviewable.payload.via_email}} - {{d-icon "far-envelope" title="post.via_email"}} + {{d-icon "envelope" title="post.via_email"}} {{/if}} diff --git a/app/assets/javascripts/discourse/app/templates/components/suggested-topics.hbs b/app/assets/javascripts/discourse/app/templates/components/suggested-topics.hbs index 77ad43678b..0725c3d6ed 100644 --- a/app/assets/javascripts/discourse/app/templates/components/suggested-topics.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/suggested-topics.hbs @@ -1,4 +1,6 @@