Version bump

This commit is contained in:
Martin Brennan 2022-11-28 11:00:16 +10:00
commit d93e5a84d1
No known key found for this signature in database
GPG Key ID: A08063EEF3EA26A4
690 changed files with 26578 additions and 16562 deletions

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -25,6 +25,11 @@
<AdminReport @dataSourceName="user_flagging_ratio" @filters={{this.lastWeekfilters}} @reportOptions={{this.userFlaggingRatioOptions}} />
<PluginOutlet @name="admin-dashboard-moderation-bottom" @tagName="span" @connectorTagName="div" />
<PluginOutlet
@name="admin-dashboard-moderation-bottom"
@tagName="span"
@connectorTagName="div"
@args={{hash filters=this.lastWeekfilters}}
/>
</div>
</div>

View File

@ -38,7 +38,7 @@
{{#if this.model.email}}
<a href="mailto:{{this.model.email}}">{{this.model.email}}</a>
{{else}}
<DButton @class="btn-default" @action={{route-action "checkEmail"}} @actionParam={{this.model}} @icon="far-envelope" @label="admin.users.check_email.text" @title="admin.users.check_email.title" />
<DButton @class="btn-default" @action={{route-action "checkEmail"}} @actionParam={{this.model}} @icon="envelope" @label="admin.users.check_email.text" @title="admin.users.check_email.title" />
{{/if}}
</div>
<div class="controls">
@ -65,7 +65,7 @@
{{i18n "user.email.no_secondary"}}
{{/if}}
{{else}}
<DButton @action={{route-action "checkEmail"}} @class="btn-default" @actionParam={{this.model}} @icon="far-envelope" @label="admin.users.check_email.text" @title="admin.users.check_email.title" />
<DButton @action={{route-action "checkEmail"}} @class="btn-default" @actionParam={{this.model}} @icon="envelope" @label="admin.users.check_email.text" @title="admin.users.check_email.title" />
{{/if}}
</div>
@ -99,7 +99,7 @@
{{#if this.associatedAccountsLoaded}}
{{this.associatedAccounts}}
{{else}}
<DButton @class="btn-default" @action={{route-action "checkEmail"}} @actionParam={{this.model}} @icon="far-envelope" @label="admin.users.check_email.text" @title="admin.users.check_email.title" />
<DButton @class="btn-default" @action={{route-action "checkEmail"}} @actionParam={{this.model}} @icon="envelope" @label="admin.users.check_email.text" @title="admin.users.check_email.title" />
{{/if}}
</div>
</div>
@ -561,7 +561,7 @@
{{#if this.ssoExternalEmail}}
<div class="value">{{this.ssoExternalEmail}}</div>
{{else}}
<DButton @class="btn-default" @action={{action "checkSsoEmail"}} @actionParam={{this.model}} @icon="far-envelope" @label="admin.users.check_email.text" @title="admin.users.check_email.title" />
<DButton @class="btn-default" @action={{action "checkSsoEmail"}} @actionParam={{this.model}} @icon="envelope" @label="admin.users.check_email.text" @title="admin.users.check_email.title" />
{{/if}}
</div>
{{/if}}

View File

@ -5,6 +5,7 @@
"paths": {
"admin/*": ["./addon/*"],
"discourse/*": ["../discourse/app/*"],
"discourse/tests/*": ["../discourse/tests/*"],
"discourse-common/*": ["../discourse-common/addon/*"],
"pretty-text/*": ["../pretty-text/addon/*"],
}

View File

@ -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",

View File

@ -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,

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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;

View File

@ -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));
}
}

View File

@ -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;
},

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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" }
);
}
}

View File

@ -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);

View File

@ -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",

View File

@ -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",

View File

@ -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";
},
};

View File

@ -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",

View File

@ -5,6 +5,7 @@
"paths": {
"discourse-widget-hbs/*": ["./addon/*"],
"discourse/*": ["../discourse/app/*"],
"discourse/tests/*": ["../discourse/tests/*"],
"discourse-common/*": ["../discourse-common/addon/*"]
}
},

View File

@ -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",

View File

@ -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);
}
}
},

View File

@ -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 [];

View File

@ -1,7 +0,0 @@
import DButton from "discourse/components/d-button";
export default DButton.extend({
click() {
$("input.bulk-select:not(checked)").click();
},
});

View File

@ -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);
}

View File

@ -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);
},

View File

@ -0,0 +1,9 @@
<DButton
@translatedTitle={{this.translatedTitle}}
@label={{@label}}
@action={{@action}}
@icon={{@icon}}
@forwardEvent={{@forwardEvent}}
class="btn-primary create {{if @disabledSubmit "disabled"}}"
...attributes
/>

View File

@ -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+") });
}
}

View File

@ -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");
}
},

View File

@ -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;
}
},
});
}
}

View File

@ -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) => {

View File

@ -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;

View File

@ -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
);

View File

@ -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" }
);
}

View File

@ -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" }
);
},
});

View File

@ -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);
}
}
},

View File

@ -1,7 +0,0 @@
import Button from "discourse/components/d-button";
export default Button.extend({
label: "topic.reply.title",
icon: "reply",
action: "showLogin",
});

View File

@ -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",
});
}
},

View File

@ -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];

View File

@ -1,4 +1,4 @@
<DSection @pageClass="has-sidebar" @class="sidebar-container" @scrollTop={{false}}>
<DSection @pageClass="has-sidebar" @id="d-sidebar" @class="sidebar-container" @scrollTop={{false}}>
<Sidebar::Sections @currentUser={{this.currentUser}} @collapsableSections={{true}} />
<Sidebar::Footer />
</DSection>

View File

@ -2,8 +2,9 @@
<DButton
@title="sidebar.toggle_section"
@class="sidebar-section-header sidebar-section-header-collapsable btn-flat"
@action={{@toggleSectionDisplay}} >
@action={{@toggleSectionDisplay}}
@ariaExpanded={{@isExpanded}}
@ariaControls={{@sidebarSectionContentID}} >
{{yield}}
</DButton>
{{else}}

View File

@ -1,5 +1,5 @@
{{#if @prefixType}}
<span class={{concat-class "sidebar-section-link-prefix" @prefixType @prefixCSSClass}} style={{html-safe (concat "color: " @prefixColor)}}>
<span class={{concat-class "sidebar-section-link-prefix" @prefixType @prefixCSSClass}} style={{if @prefixColor (html-safe (concat "color: " @prefixColor))}}>
{{#if (eq @prefixType "image")}}
<img src={{@prefixValue}} class="prefix-image">
{{/if}}

View File

@ -1,5 +1,5 @@
{{#if this.shouldDisplay}}
<div class="sidebar-section-link-wrapper">
<li class="sidebar-section-link-wrapper" {{did-insert this.didInsert this.args}}>
{{#if @href}}
<a href={{@href}} rel="noopener noreferrer" target="_blank" class={{this.classNames}} title={{@title}}>
<Sidebar::SectionLinkPrefix
@ -68,5 +68,5 @@
{{/if}}
</Sidebar::SectionLinkTo>
{{/if}}
</div>
</li>
{{/if}}

View File

@ -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;

View File

@ -1,7 +1,12 @@
{{#if this.displaySection}}
<div class={{concat "sidebar-section-wrapper sidebar-section-" @sectionName}}>
<div class="sidebar-section-header-wrapper sidebar-row">
<Sidebar::SectionHeader @collapsable={{@collapsable}} @toggleSectionDisplay={{this.toggleSectionDisplay}}>
<Sidebar::SectionHeader
@collapsable={{@collapsable}}
@sidebarSectionContentID={{this.sidebarSectionContentID}}
@toggleSectionDisplay={{this.toggleSectionDisplay}}
@isExpanded={{this.displaySectionContent}}
>
{{#if @collapsable}}
<span class="sidebar-section-header-caret">
{{d-icon this.headerCaretIcon}}
@ -36,9 +41,9 @@
</div>
{{#if this.displaySectionContent}}
<div class="sidebar-section-content">
<ul class="sidebar-section-content" id={{this.sidebarSectionContentID}}>
{{yield}}
</div>
</ul>
{{/if}}
</div>
{{/if}}

View File

@ -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() {

View File

@ -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,

View File

@ -43,6 +43,7 @@
@hoverAction={{link.hoverAction}}
@hoverTitle={{link.hoverTitle}}
@currentWhen={{link.currentWhen}}
@didInsert={{link.didInsert}}
@willDestroy={{link.willDestroy}}
@content={{link.text}} />
{{/each}}

View File

@ -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");
},
});

View File

@ -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() {

View File

@ -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"),

View File

@ -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"),

View File

@ -0,0 +1,10 @@
<div class="empty-state">
<span class="empty-state-title">
{{i18n "user.no_likes_title"}}
</span>
<div class="empty-state-body">
<p>
{{html-safe (i18n "user.no_likes_body" preferencesUrl=(get-url "/my/preferences/notifications"))}}
</p>
</div>
</div>

View File

@ -8,4 +8,8 @@ export default class UserMenuLikesNotificationsList extends UserMenuNotification
dismissWarningModal() {
return null;
}
get emptyStateComponent() {
return "user-menu/likes-list-empty-state";
}
}

View File

@ -28,7 +28,7 @@
{{#if @showPrivateMessages}}
<li class="private-messages">
<LinkTo @route="userPrivateMessages">
{{d-icon "far-envelope"}}
{{d-icon "envelope"}}
<span>{{i18n "user.private_messages"}}</span>
</LinkTo>
</li>

View File

@ -7,7 +7,7 @@
</ol>
{{/if}}
<HorizontalOverflowNav>
<HorizontalOverflowNav @className="messages-nav">
{{#if @isPersonal}}
<li class="messages-latest">
<LinkTo @route="userPrivateMessages.index" @model={{@user}}>
@ -47,39 +47,37 @@
</li>
{{/if}}
{{#each @user.groups as |group|}}
{{#if (and @isGroup (eq @groupFilter group.name))}}
{{#if @viewingSelf}}
<li class="messages-group-latest">
<LinkTo @route="userPrivateMessages.group" @model={{group.name}}>
{{d-icon "envelope"}}
<span>{{i18n "categories.latest"}}</span>
</LinkTo>
</li>
{{#if @isGroup}}
<li class="messages-group-latest">
<LinkTo @route="userPrivateMessages.group" @model={{@group.name}}>
{{d-icon "envelope"}}
<span>{{i18n "categories.latest"}}</span>
</LinkTo>
</li>
<li class="messages-group-new">
<LinkTo @route="userPrivateMessages.groupNew" @model={{group.name}} class="new">
{{d-icon "exclamation-circle"}}
<span>{{@newLinkText}}</span>
</LinkTo>
</li>
{{#if @viewingSelf}}
<li class="messages-group-new">
<LinkTo @route="userPrivateMessages.groupNew" @model={{@group.name}} class="new">
{{d-icon "exclamation-circle"}}
<span>{{@newLinkText}}</span>
</LinkTo>
</li>
<li class="messages-group-unread">
<LinkTo @route="userPrivateMessages.groupUnread" @model={{group.name}} class="unread">
{{d-icon "plus-circle"}}
<span>{{@unreadLinkText}}</span>
</LinkTo>
</li>
{{/if}}
<li class="messages-group-unread">
<LinkTo @route="userPrivateMessages.groupUnread" @model={{@group.name}} class="unread">
{{d-icon "plus-circle"}}
<span>{{@unreadLinkText}}</span>
</LinkTo>
</li>
<li class="messages-group-archive">
<LinkTo @route="userPrivateMessages.groupArchive" @model={{group.name}}>
<LinkTo @route="userPrivateMessages.groupArchive" @model={{@group.name}}>
{{d-icon "archive"}}
<span>{{i18n "user.messages.archive"}}</span>
</LinkTo>
</li>
{{/if}}
{{/each}}
{{/if}}
{{#if this.displayTags}}
<li class="tags">

View File

@ -4,58 +4,58 @@
<span>{{i18n "user.preferences_nav.account"}}</span>
</LinkTo>
</li>
<li class="nav-security">
<LinkTo @route="preferences.security">
{{d-icon "lock"}}
<span>{{i18n "user.preferences_nav.security"}}</span>
</LinkTo>
</li>
<li class="nav-profile">
<LinkTo @route="preferences.profile">
{{d-icon "user"}}
<span>{{i18n "user.preferences_nav.profile"}}</span>
</LinkTo>
</li>
<li class="nav-emails">
<LinkTo @route="preferences.emails">
{{d-icon "envelope"}}
<span>{{i18n "user.preferences_nav.emails"}}</span>
</LinkTo>
</li>
<li class="nav-notifications">
<LinkTo @route="preferences.notifications">
{{d-icon "bell"}}
<span>{{i18n "user.preferences_nav.notifications"}}</span>
</LinkTo>
</li>
{{#if @model.can_change_tracking_preferences}}
<li class="indent nav-categories">
<LinkTo @route="preferences.categories">
{{d-icon "folder"}}
<span>{{i18n "user.preferences_nav.categories"}}</span>
<li class="nav-tracking">
<LinkTo @route="preferences.tracking">
{{d-icon "plus"}}
<span>{{i18n "user.preferences_nav.tracking"}}</span>
</LinkTo>
</li>
{{/if}}
<li class="indent nav-users">
<LinkTo @route="preferences.users">
{{d-icon "users"}}
<span>{{i18n "user.preferences_nav.users"}}</span>
</LinkTo>
</li>
{{#if (and @model.can_change_tracking_preferences @siteSettings.tagging_enabled)}}
<li class="indent nav-tags">
<LinkTo @route="preferences.tags">
{{d-icon "tag"}}
<span>{{i18n "user.preferences_nav.tags"}}</span>
</LinkTo>
</li>
{{/if}}
<li class="nav-interface">
<LinkTo @route="preferences.interface">
{{d-icon "desktop"}}
<span>{{i18n "user.preferences_nav.interface"}}</span>
</LinkTo>
</li>
{{#if @siteSettings.enable_experimental_sidebar_hamburger}}
<li class="indent nav-sidebar">
<LinkTo @route="preferences.sidebar">
@ -64,11 +64,6 @@
</LinkTo>
</li>
{{/if}}
<PluginOutlet @name="user-preferences-nav-under-interface" @connectorTagName="div" @args={{hash model=@model}} />
<li class="nav-apps">
<LinkTo @route="preferences.apps">
{{d-icon "mobile-alt"}}
<span>{{i18n "user.preferences_nav.apps"}}</span>
</LinkTo>
</li>
<PluginOutlet @name="user-preferences-nav" @connectorTagName="li" @args={{hash model=@model}} />

View File

@ -0,0 +1,6 @@
<div class="control-group private-messages">
<label class="control-label">{{i18n "user.private_messages"}}</label>
<div class="controls">
<PreferenceCheckbox @labelKey="user.allow_private_messages" @checked={{@model.user_option.allow_private_messages}} />
</div>
</div>

View File

@ -0,0 +1,53 @@
<div class="control-group category-notifications">
<label class="control-label">{{i18n "user.categories_settings"}}</label>
<div class="controls tracking-controls tracking-controls__watched-categories">
<label>{{d-icon "d-watching"}} {{i18n "user.watched_categories"}}</label>
{{#if @canSee}}
<a class="show-tracking" href={{@model.watchingTopicsPath}}>{{i18n "user.tracked_topics_link"}}</a>
{{/if}}
<CategorySelector @categories={{@model.watchedCategories}} @blockedCategories={{@selectedCategories}} @onChange={{action (mut @model.watchedCategories)}} />
</div>
<div class="instructions">{{i18n "user.watched_categories_instructions"}}</div>
<div class="controls tracking-controls tracking-controls__tracked-categories">
<label>{{d-icon "d-tracking"}} {{i18n "user.tracked_categories"}}</label>
{{#if @canSee}}
<a class="show-tracking" href={{@model.trackingTopicsPath}}>{{i18n "user.tracked_topics_link"}}</a>
{{/if}}
<CategorySelector @categories={{@model.trackedCategories}} @blockedCategories={{@selectedCategories}} @onChange={{action (mut @model.trackedCategories)}} />
</div>
<div class="instructions">{{i18n "user.tracked_categories_instructions"}}</div>
<div class="controls tracking-controls tracking-controls__watched-first-categories">
<label>{{d-icon "d-watching-first"}} {{i18n "user.watched_first_post_categories"}}</label>
<CategorySelector @categories={{@model.watchedFirstPostCategories}} @blockedCategories={{@selectedCategories}} @onChange={{action (mut @model.watchedFirstPostCategories)}} />
</div>
<div class="instructions">{{i18n "user.watched_first_post_categories_instructions"}}</div>
{{#if @siteSettings.mute_all_categories_by_default}}
<div class="controls tracking-controls tracking-controls__regular-categories">
<label>{{d-icon "d-regular"}} {{i18n "user.regular_categories"}}</label>
<CategorySelector @categories={{@model.regularCategories}} @blockedCategories={{@selectedCategories}} @onChange={{action (mut @model.regularCategories)}} />
</div>
<div class="instructions">{{i18n "user.regular_categories_instructions"}}</div>
{{else}}
<div class="controls tracking-controls tracking-controls__muted-categories">
<label>{{d-icon "d-muted"}} {{i18n "user.muted_categories"}}</label>
{{#if @canSee}}
<a class="show-tracking" href={{@model.mutedTopicsPath}}>{{i18n "user.tracked_topics_link"}}</a>
{{/if}}
<CategorySelector @categories={{@model.mutedCategories}} @blockedCategories={{@selectedCategories}} @onChange={{action (mut @model.mutedCategories)}} />
</div>
<div class="instructions">{{i18n (if @hideMutedTags "user.muted_categories_instructions" "user.muted_categories_instructions_dont_hide")}}</div>
{{/if}}
</div>
<PluginOutlet @name="user-preferences-categories" @tagName="span" @connectorTagName="div" @args={{hash model=@model save=@save}} />
<br>
<PluginOutlet @name="user-custom-controls" @tagName="span" @connectorTagName="div" @args={{hash model=@model}} />

View File

@ -0,0 +1,45 @@
{{#if @siteSettings.tagging_enabled}}
<div class="control-group tag-notifications">
<label class="control-label">{{i18n "user.tag_settings"}}</label>
<div class="controls tracking-controls tracking-controls__watched-tags">
<label>{{d-icon "d-watching" class="icon watching"}} {{i18n "user.watched_tags"}}</label>
<TagChooser @tags={{@model.watched_tags}} @blockedTags={{@selectedTags}} @everyTag={{true}} @unlimitedTagCount={{true}} @options={{hash
allowAny=false
}} />
</div>
<div class="instructions">{{i18n "user.watched_tags_instructions"}}</div>
<div class="controls tracking-controls tracking-controls__tracked-tags">
<label>{{d-icon "d-tracking" class="icon tracking"}} {{i18n "user.tracked_tags"}}</label>
<TagChooser @tags={{@model.tracked_tags}} @blockedTags={{@selectedTags}} @everyTag={{true}} @unlimitedTagCount={{true}} @options={{hash
allowAny=false
}} />
</div>
<div class="instructions">{{i18n "user.tracked_tags_instructions"}}</div>
<div class="controls tracking-controls tracking-controls__watched-first-post-tags">
<label>{{d-icon "d-watching-first" class="icon watching-first-post"}} {{i18n "user.watched_first_post_tags"}}</label>
<TagChooser @tags={{@model.watching_first_post_tags}} @blockedTags={{@selectedTags}} @everyTag={{true}} @unlimitedTagCount={{true}} @options={{hash
allowAny=false
}} />
</div>
<div class="instructions">
{{i18n "user.watched_first_post_tags_instructions"}}
</div>
<div class="controls tracking-controls tracking-controls__muted-tags">
<label>{{d-icon "d-muted" class="icon muted"}} {{i18n "user.muted_tags"}}</label>
<TagChooser @tags={{@model.muted_tags}} @blockedTags={{@selectedTags}} @everyTag={{true}} @unlimitedTagCount={{true}} @options={{hash
allowAny=false
}} />
</div>
<div class="instructions">{{i18n "user.muted_tags_instructions"}}</div>
</div>
<PluginOutlet @name="user-preferences-tags" @connectorTagName="div" @args={{hash model=@model save=@save}} />
<PluginOutlet @name="user-custom-controls" @connectorTagName="div" @args={{hash model=@model}} />
{{/if}}

View File

@ -0,0 +1,16 @@
<div class="user-preferences_tracking-topics-controls">
<div class="controls controls-dropdown">
<label>{{i18n "user.new_topic_duration.label"}}</label>
<ComboBox @class="duration" @valueProperty="value" @content={{@considerNewTopicOptions}} @value={{@model.user_option.new_topic_duration_minutes}} @onChange={{action (mut @model.user_option.new_topic_duration_minutes)}} />
</div>
<div class="controls controls-dropdown">
<label>{{i18n "user.auto_track_topics"}}</label>
<ComboBox @valueProperty="value" @content={{@autoTrackDurations}} @value={{@model.user_option.auto_track_topics_after_msecs}} @onChange={{action (mut @model.user_option.auto_track_topics_after_msecs)}} />
</div>
<div class="controls controls-dropdown">
<label>{{i18n "user.notification_level_when_replying"}}</label>
<ComboBox @valueProperty="value" @content={{@notificationLevelsForReplying}} @value={{@model.user_option.notification_level_when_replying}} @onChange={{action (mut @model.user_option.notification_level_when_replying)}} />
</div>
</div>

View File

@ -0,0 +1,35 @@
{{#if @model.userApiKeys}}
<div class="control-group pref-user-api-keys">
<label class="control-label pref-user-api-keys__label">{{i18n "user.apps"}}</label>
<div class="controls">
{{#each @model.userApiKeys as |key|}}
<div>
<span class="pref-user-api-keys__application-name">{{key.application_name}}</span>
{{#if key.revoked}}
<DButton @action={{route-action "undoRevokeApiKey"}} @actionParam={{key}} @class="btn" @label="user.undo_revoke_access" />
{{else}}
<DButton @action={{route-action "revokeApiKey"}} @actionParam={{key}} @class="btn" @label="user.revoke_access" />
{{/if}}
<p>
<ul class="pref-user-api-keys__scopes-list">
{{#each key.scopes as |scope|}}
<li class="pref-user-api-keys__scopes-list-item">{{scope}}</li>
{{/each}}
</ul>
</p>
<p class="pref-user-api-keys__created-at">
<span>{{i18n "user.api_approved"}}</span> {{bound-date key.created_at}}
</p>
<p class="pref-user-api-keys__last-used-at">
<span>{{i18n "user.api_last_used_at"}}</span> {{bound-date key.last_used_at}}
</p>
</div>
{{/each}}
</div>
</div>
{{/if}}

View File

@ -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 `<UserSelector>` component is deprecated. Please use `<EmailGroupUserChooser>` instead.",
{ since: "2.7", dropFrom: "2.8", id: "discourse.user-selector-component" }
);
},

View File

@ -0,0 +1 @@
<span {{did-insert this.showUserTip}}></span>

View File

@ -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);
}
}

View File

@ -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

View File

@ -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);
},
},

View File

@ -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);

View File

@ -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"),
});
},

View File

@ -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)();
},

View File

@ -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"),

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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", {

View File

@ -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;

View File

@ -2,6 +2,12 @@
<bootstrap-content key="html-tag">
<head>
<!--
👋 Greetings Discourse Developer. This HTML was generated by the ember-cli proxy. If you're looking for
<head> content generated by Rails, you'll need to start the server with `ALLOW_EMBER_CLI_PROXY_BYPASS=1`
and then visit the Rails port (e.g. `localhost:3000`) directly. Be sure to keep ember-cli running so
that JS assets continue to be re-compiled when changes are made.
-->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Discourse - Ember CLI</title>

View File

@ -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);
}
});
},
};

View File

@ -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;

View File

@ -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);

View File

@ -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);
}
}
},
};

View File

@ -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()) {

View File

@ -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
)
);
});
});
},
};

View File

@ -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);

View File

@ -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)) {

View File

@ -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.",

View File

@ -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",
});
}

View File

@ -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 = `<svg class="fa d-icon d-icon-${matchingSeenHashtag.icon} svg-icon svg-node"><use href="#${matchingSeenHashtag.icon}"></use></svg><span>${matchingSeenHashtag.text}</span>`;
hashtagSpan.replaceWith(link);
}
});
}

View File

@ -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",
});
}

View File

@ -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",
});
}

View File

@ -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;

Some files were not shown because too many files have changed in this diff Show More