Version bump
This commit is contained in:
commit
d93e5a84d1
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@ -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
|
||||
|
||||
7
Gemfile
7
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'
|
||||
|
||||
57
Gemfile.lock
57
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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
"paths": {
|
||||
"admin/*": ["./addon/*"],
|
||||
"discourse/*": ["../discourse/app/*"],
|
||||
"discourse/tests/*": ["../discourse/tests/*"],
|
||||
"discourse-common/*": ["../discourse-common/addon/*"],
|
||||
"pretty-text/*": ["../pretty-text/addon/*"],
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
30
app/assets/javascripts/bootstrap-json/package.json
Normal file
30
app/assets/javascripts/bootstrap-json/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
24
app/assets/javascripts/dialog-holder/package.json
Normal file
24
app/assets/javascripts/dialog-holder/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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" }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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";
|
||||
},
|
||||
};
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
"paths": {
|
||||
"discourse-widget-hbs/*": ["./addon/*"],
|
||||
"discourse/*": ["../discourse/app/*"],
|
||||
"discourse/tests/*": ["../discourse/tests/*"],
|
||||
"discourse-common/*": ["../discourse-common/addon/*"]
|
||||
}
|
||||
},
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -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 [];
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
import DButton from "discourse/components/d-button";
|
||||
|
||||
export default DButton.extend({
|
||||
click() {
|
||||
$("input.bulk-select:not(checked)").click();
|
||||
},
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
<DButton
|
||||
@translatedTitle={{this.translatedTitle}}
|
||||
@label={{@label}}
|
||||
@action={{@action}}
|
||||
@icon={{@icon}}
|
||||
@forwardEvent={{@forwardEvent}}
|
||||
class="btn-primary create {{if @disabledSubmit "disabled"}}"
|
||||
...attributes
|
||||
/>
|
||||
@ -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+") });
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
);
|
||||
@ -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" }
|
||||
);
|
||||
}
|
||||
|
||||
@ -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" }
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
import Button from "discourse/components/d-button";
|
||||
|
||||
export default Button.extend({
|
||||
label: "topic.reply.title",
|
||||
icon: "reply",
|
||||
action: "showLogin",
|
||||
});
|
||||
@ -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",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -43,6 +43,7 @@
|
||||
@hoverAction={{link.hoverAction}}
|
||||
@hoverTitle={{link.hoverTitle}}
|
||||
@currentWhen={{link.currentWhen}}
|
||||
@didInsert={{link.didInsert}}
|
||||
@willDestroy={{link.willDestroy}}
|
||||
@content={{link.text}} />
|
||||
{{/each}}
|
||||
|
||||
@ -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");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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>
|
||||
@ -8,4 +8,8 @@ export default class UserMenuLikesNotificationsList extends UserMenuNotification
|
||||
dismissWarningModal() {
|
||||
return null;
|
||||
}
|
||||
|
||||
get emptyStateComponent() {
|
||||
return "user-menu/likes-list-empty-state";
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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}} />
|
||||
|
||||
@ -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>
|
||||
@ -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}} />
|
||||
@ -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}}
|
||||
@ -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>
|
||||
@ -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}}
|
||||
@ -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" }
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@ -0,0 +1 @@
|
||||
<span {{did-insert this.showUserTip}}></span>
|
||||
35
app/assets/javascripts/discourse/app/components/user-tip.js
Normal file
35
app/assets/javascripts/discourse/app/components/user-tip.js
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
},
|
||||
},
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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"),
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -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)();
|
||||
},
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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", {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -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()) {
|
||||
|
||||
@ -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
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -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);
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
16
app/assets/javascripts/discourse/app/lib/narrow-desktop.js
Normal file
16
app/assets/javascripts/discourse/app/lib/narrow-desktop.js
Normal 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
Reference in New Issue
Block a user