diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e7d81ec8da..129966e580 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -176,14 +176,14 @@ jobs: - name: Plugin System Tests if: matrix.build_type == 'system' && matrix.target == 'plugins' - run: LOAD_PLUGINS=1 bin/rspec plugins/*/spec/system + run: LOAD_PLUGINS=1 bin/rspec plugins/*/spec/system --format documentation --profile - name: Upload failed system test screenshots uses: actions/upload-artifact@v3 if: matrix.build_type == 'system' && failure() with: name: failed-system-test-screenshots - path: tmp/screenshots/*.png + path: tmp/capybara/*.png - name: Check Annotations if: matrix.build_type == 'annotations' diff --git a/Gemfile.lock b/Gemfile.lock index 21a03955f2..6a00754e7f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -97,7 +97,6 @@ GEM xpath (~> 3.2) cbor (0.5.9.6) certified (1.0.0) - childprocess (4.1.0) chunky_png (1.4.0) coderay (1.1.3) colored2 (3.1.2) @@ -115,7 +114,7 @@ GEM debug_inspector (1.1.0) diff-lcs (1.5.0) diffy (3.4.2) - digest (3.1.0) + digest (3.1.1) discourse-fonts (0.0.9) discourse-seed-fu (2.3.12) activerecord (>= 3.1) @@ -128,7 +127,7 @@ GEM regexp_parser (~> 2.2) email_reply_trimmer (0.1.13) erubi (1.11.0) - excon (0.94.0) + excon (0.95.0) execjs (2.8.1) exifr (1.3.10) fabrication (2.30.0) @@ -168,7 +167,7 @@ GEM image_size (3.2.0) in_threads (1.6.0) jmespath (1.6.2) - json (2.6.2) + json (2.6.3) json-schema (3.0.0) addressable (>= 2.8) json_schemer (0.2.23) @@ -230,21 +229,21 @@ GEM net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.1.3) + net-protocol (0.2.1) timeout net-smtp (0.3.3) net-protocol nio4r (2.5.8) - nokogiri (1.13.9) + nokogiri (1.13.10) mini_portile2 (~> 2.8.0) racc (~> 1.4) - nokogiri (1.13.9-aarch64-linux) + nokogiri (1.13.10-aarch64-linux) racc (~> 1.4) - nokogiri (1.13.9-arm64-darwin) + nokogiri (1.13.10-arm64-darwin) racc (~> 1.4) - nokogiri (1.13.9-x86_64-darwin) + nokogiri (1.13.10-x86_64-darwin) racc (~> 1.4) - nokogiri (1.13.9-x86_64-linux) + nokogiri (1.13.10-x86_64-linux) racc (~> 1.4) oauth (1.1.0) oauth-tty (~> 1.0, >= 1.0.1) @@ -300,11 +299,11 @@ GEM pry (>= 0.13, < 0.15) pry-rails (0.3.9) pry (>= 0.10.4) - public_suffix (5.0.0) + public_suffix (5.0.1) puma (6.0.0) nio4r (~> 2.0) r2 (0.2.7) - racc (1.6.0) + racc (1.6.1) rack (2.2.4) rack-mini-profiler (3.0.0) rack (>= 1.2.0) @@ -342,7 +341,7 @@ GEM msgpack (>= 0.4.3) optimist (>= 3.0.0) rchardet (1.8.0) - redis (4.7.1) + redis (4.8.0) redis-namespace (1.9.0) redis (>= 4) regexp_parser (2.6.1) @@ -367,7 +366,7 @@ GEM rspec-html-matchers (0.10.0) nokogiri (~> 1) rspec (>= 3.0.0.a) - rspec-mocks (3.12.0) + rspec-mocks (3.12.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-rails (6.0.1) @@ -386,7 +385,7 @@ GEM json-schema (>= 2.2, < 4.0) railties (>= 3.1, < 7.1) rspec-core (>= 2.14) - rubocop (1.39.0) + rubocop (1.40.0) json (~> 2.3) parallel (~> 1.10) parser (>= 3.1.2.1) @@ -422,8 +421,7 @@ GEM sprockets (> 3.0) sprockets-rails tilt - selenium-webdriver (4.6.1) - childprocess (>= 0.5, < 5.0) + selenium-webdriver (4.7.1) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) @@ -451,10 +449,10 @@ GEM sprockets (>= 3.0.0) sshkey (2.0.0) stackprof (0.2.23) - test-prof (1.0.11) + test-prof (1.1.0) thor (1.2.1) tilt (2.0.11) - timeout (0.3.0) + timeout (0.3.1) tzinfo (2.0.5) concurrent-ruby (~> 1.0) uglifier (4.2.0) @@ -467,7 +465,7 @@ GEM kgio (~> 2.6) raindrops (~> 0.7) uniform_notifier (1.16.0) - uri (0.11.0) + uri (0.12.0) uri_template (0.7.0) version_gem (1.1.1) webdrivers (5.2.0) diff --git a/app/assets/javascripts/admin/addon/components/admin-penalty-similar-users.js b/app/assets/javascripts/admin/addon/components/admin-penalty-similar-users.js new file mode 100644 index 0000000000..0e0158e868 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/admin-penalty-similar-users.js @@ -0,0 +1,29 @@ +import Component from "@ember/component"; +import { action } from "@ember/object"; +import discourseComputed from "discourse-common/utils/decorators"; + +export default Component.extend({ + tagName: "", + + @discourseComputed("type") + penaltyField(penaltyType) { + if (penaltyType === "suspend") { + return "can_be_suspended"; + } else if (penaltyType === "silence") { + return "can_be_silenced"; + } + }, + + @action + selectUserId(userId, event) { + if (!this.selectedUserIds) { + return; + } + + if (event.target.checked) { + this.selectedUserIds.pushObject(userId); + } else { + this.selectedUserIds.removeObject(userId); + } + }, +}); diff --git a/app/assets/javascripts/admin/addon/components/admin-web-hook-event-chooser.js b/app/assets/javascripts/admin/addon/components/admin-web-hook-event-chooser.js deleted file mode 100644 index 1c8e59fd27..0000000000 --- a/app/assets/javascripts/admin/addon/components/admin-web-hook-event-chooser.js +++ /dev/null @@ -1,47 +0,0 @@ -import Component from "@ember/component"; -import I18n from "I18n"; -import { alias } from "@ember/object/computed"; -import discourseComputed from "discourse-common/utils/decorators"; - -export default Component.extend({ - classNames: ["hook-event"], - typeName: alias("type.name"), - - @discourseComputed("typeName") - name(typeName) { - return I18n.t(`admin.web_hooks.${typeName}_event.name`); - }, - - @discourseComputed("typeName") - details(typeName) { - return I18n.t(`admin.web_hooks.${typeName}_event.details`); - }, - - @discourseComputed("model.[]", "typeName") - eventTypeExists(eventTypes, typeName) { - return eventTypes.any((event) => event.name === typeName); - }, - - @discourseComputed("eventTypeExists") - enabled: { - get(eventTypeExists) { - return eventTypeExists; - }, - set(value, eventTypeExists) { - const type = this.type; - const model = this.model; - // add an association when not exists - if (value !== eventTypeExists) { - if (value) { - model.addObject(type); - } else { - model.removeObjects( - model.filter((eventType) => eventType.name === type.name) - ); - } - } - - return value; - }, - }, -}); diff --git a/app/assets/javascripts/admin/addon/components/admin-web-hook-event.js b/app/assets/javascripts/admin/addon/components/admin-web-hook-event.js deleted file mode 100644 index dd13384a2d..0000000000 --- a/app/assets/javascripts/admin/addon/components/admin-web-hook-event.js +++ /dev/null @@ -1,110 +0,0 @@ -import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter"; -import Component from "@ember/component"; -import I18n from "I18n"; -import { ajax } from "discourse/lib/ajax"; -import discourseComputed from "discourse-common/utils/decorators"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { inject as service } from "@ember/service"; - -export default Component.extend({ - tagName: "li", - expandDetails: null, - expandDetailsRequestKey: "request", - expandDetailsResponseKey: "response", - dialog: service(), - - @discourseComputed("model.status") - statusColorClasses(status) { - if (!status) { - return ""; - } - - if (status >= 200 && status <= 299) { - return "text-successful"; - } else { - return "text-danger"; - } - }, - - @discourseComputed("model.created_at") - createdAt(createdAt) { - return moment(createdAt).format("YYYY-MM-DD HH:mm:ss"); - }, - - @discourseComputed("model.duration") - completion(duration) { - const seconds = Math.floor(duration / 10.0) / 100.0; - return I18n.t("admin.web_hooks.events.completed_in", { count: seconds }); - }, - - @discourseComputed("expandDetails") - expandRequestIcon(expandDetails) { - return expandDetails === this.expandDetailsRequestKey - ? "ellipsis-h" - : "ellipsis-v"; - }, - - @discourseComputed("expandDetails") - expandResponseIcon(expandDetails) { - return expandDetails === this.expandDetailsResponseKey - ? "ellipsis-h" - : "ellipsis-v"; - }, - - actions: { - redeliver() { - return this.dialog.yesNoConfirm({ - message: I18n.t("admin.web_hooks.events.redeliver_confirm"), - didConfirm: () => { - return ajax( - `/admin/api/web_hooks/${this.get( - "model.web_hook_id" - )}/events/${this.get("model.id")}/redeliver`, - { type: "POST" } - ) - .then((json) => { - this.set("model", json.web_hook_event); - }) - .catch(popupAjaxError); - }, - }); - }, - - toggleRequest() { - const expandDetailsKey = this.expandDetailsRequestKey; - - if (this.expandDetails !== expandDetailsKey) { - let headers = Object.assign( - { - "Request URL": this.get("model.request_url"), - "Request method": "POST", - }, - ensureJSON(this.get("model.headers")) - ); - this.setProperties({ - headers: plainJSON(headers), - body: prettyJSON(this.get("model.payload")), - expandDetails: expandDetailsKey, - bodyLabel: I18n.t("admin.web_hooks.events.payload"), - }); - } else { - this.set("expandDetails", null); - } - }, - - toggleResponse() { - const expandDetailsKey = this.expandDetailsResponseKey; - - if (this.expandDetails !== expandDetailsKey) { - this.setProperties({ - headers: plainJSON(this.get("model.response_headers")), - body: this.get("model.response_body"), - expandDetails: expandDetailsKey, - bodyLabel: I18n.t("admin.web_hooks.events.body"), - }); - } else { - this.set("expandDetails", null); - } - }, - }, -}); diff --git a/app/assets/javascripts/admin/addon/components/admin-web-hook-status.js b/app/assets/javascripts/admin/addon/components/admin-web-hook-status.js deleted file mode 100644 index 880e91621c..0000000000 --- a/app/assets/javascripts/admin/addon/components/admin-web-hook-status.js +++ /dev/null @@ -1,39 +0,0 @@ -import Component from "@ember/component"; -import I18n from "I18n"; -import discourseComputed from "discourse-common/utils/decorators"; -import { iconHTML } from "discourse-common/lib/icon-library"; -import { htmlSafe } from "@ember/template"; - -export default Component.extend({ - classes: ["text-muted", "text-danger", "text-successful", "text-muted"], - icons: ["far-circle", "times-circle", "circle", "circle"], - circleIcon: null, - deliveryStatus: null, - - @discourseComputed("deliveryStatuses", "model.last_delivery_status") - status(deliveryStatuses, lastDeliveryStatus) { - return deliveryStatuses.find((s) => s.id === lastDeliveryStatus); - }, - - @discourseComputed("status.id", "icons") - icon(statusId, icons) { - return icons[statusId - 1]; - }, - - @discourseComputed("status.id", "classes") - class(statusId, classes) { - return classes[statusId - 1]; - }, - - didReceiveAttrs() { - this._super(...arguments); - this.set( - "circleIcon", - htmlSafe(iconHTML(this.icon, { class: this.class })) - ); - this.set( - "deliveryStatus", - I18n.t(`admin.web_hooks.delivery_status.${this.get("status.name")}`) - ); - }, -}); diff --git a/app/assets/javascripts/admin/addon/components/webhook-event-chooser.hbs b/app/assets/javascripts/admin/addon/components/webhook-event-chooser.hbs new file mode 100644 index 0000000000..df4b1b6f11 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/webhook-event-chooser.hbs @@ -0,0 +1,11 @@ + diff --git a/app/assets/javascripts/admin/addon/components/webhook-event-chooser.js b/app/assets/javascripts/admin/addon/components/webhook-event-chooser.js new file mode 100644 index 0000000000..cc818e92b9 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/webhook-event-chooser.js @@ -0,0 +1,41 @@ +import Component from "@glimmer/component"; +import I18n from "I18n"; + +export default class WebhookEventChooser extends Component { + get name() { + return I18n.t(`admin.web_hooks.${this.args.type.name}_event.name`); + } + + get details() { + return I18n.t(`admin.web_hooks.${this.args.type.name}_event.details`); + } + + get eventTypeExists() { + return this.args.eventTypes.any( + (event) => event.name === this.args.type.name + ); + } + + get enabled() { + return this.eventTypeExists; + } + + set enabled(value) { + const eventTypes = this.args.eventTypes; + + // add an association when not exists + if (value === this.eventTypeExists) { + return value; + } + + if (value) { + eventTypes.addObject(this.args.type); + } else { + eventTypes.removeObjects( + eventTypes.filter((eventType) => eventType.name === this.args.type.name) + ); + } + + return value; + } +} diff --git a/app/assets/javascripts/admin/addon/components/webhook-event.hbs b/app/assets/javascripts/admin/addon/components/webhook-event.hbs new file mode 100644 index 0000000000..38cdacf79b --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/webhook-event.hbs @@ -0,0 +1,39 @@ +
  • +
    + {{@event.status}} +
    + +
    {{@event.id}}
    + +
    {{this.createdAt}}
    + +
    {{this.completion}}
    + +
    + + + +
    + + {{#if this.expandDetails}} +
    +

    {{i18n "admin.web_hooks.events.headers"}}

    +
    {{this.headers}}
    + +

    {{this.bodyLabel}}

    +
    {{this.body}}
    +
    + {{/if}} +
  • diff --git a/app/assets/javascripts/admin/addon/components/webhook-event.js b/app/assets/javascripts/admin/addon/components/webhook-event.js new file mode 100644 index 0000000000..bb6b81bb37 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/webhook-event.js @@ -0,0 +1,102 @@ +import Component from "@glimmer/component"; +import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter"; +import I18n from "I18n"; +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { inject as service } from "@ember/service"; +import { action } from "@ember/object"; +import { tracked } from "@glimmer/tracking"; + +export default class WebhookEvent extends Component { + @service dialog; + + @tracked body = ""; + @tracked bodyLabel = ""; + @tracked expandDetails = null; + @tracked headers = ""; + expandDetailsRequestKey = "request"; + expandDetailsResponseKey = "response"; + + get statusColorClasses() { + const { status } = this.args.event; + + if (!status) { + return ""; + } + + if (status >= 200 && status <= 299) { + return "text-successful"; + } else { + return "text-danger"; + } + } + + get createdAt() { + return moment(this.args.event.created_at).format("YYYY-MM-DD HH:mm:ss"); + } + + get completion() { + const seconds = Math.floor(this.args.event.duration / 10.0) / 100.0; + return I18n.t("admin.web_hooks.events.completed_in", { count: seconds }); + } + + get expandRequestIcon() { + return this.expandDetails === this.expandDetailsRequestKey + ? "ellipsis-h" + : "ellipsis-v"; + } + + get expandResponseIcon() { + return this.expandDetails === this.expandDetailsResponseKey + ? "ellipsis-h" + : "ellipsis-v"; + } + + @action + redeliver() { + return this.dialog.yesNoConfirm({ + message: I18n.t("admin.web_hooks.events.redeliver_confirm"), + didConfirm: async () => { + try { + const json = await ajax( + `/admin/api/web_hooks/${this.args.event.web_hook_id}/events/${this.args.event.id}/redeliver`, + { type: "POST" } + ); + this.args.event.setProperties(json.web_hook_event); + } catch (e) { + popupAjaxError(e); + } + }, + }); + } + + @action + toggleRequest() { + if (this.expandDetails !== this.expandDetailsRequestKey) { + const headers = { + "Request URL": this.args.event.request_url, + "Request method": "POST", + ...ensureJSON(this.args.event.headers), + }; + + this.headers = plainJSON(headers); + this.body = prettyJSON(this.args.event.payload); + this.expandDetails = this.expandDetailsRequestKey; + this.bodyLabel = I18n.t("admin.web_hooks.events.payload"); + } else { + this.expandDetails = null; + } + } + + @action + toggleResponse() { + if (this.expandDetails !== this.expandDetailsResponseKey) { + this.headers = plainJSON(this.args.event.response_headers); + this.body = this.args.event.response_body; + this.expandDetails = this.expandDetailsResponseKey; + this.bodyLabel = I18n.t("admin.web_hooks.events.body"); + } else { + this.expandDetails = null; + } + } +} diff --git a/app/assets/javascripts/admin/addon/templates/web-hooks-show-events.hbs b/app/assets/javascripts/admin/addon/components/webhook-events.hbs similarity index 54% rename from app/assets/javascripts/admin/addon/templates/web-hooks-show-events.hbs rename to app/assets/javascripts/admin/addon/components/webhook-events.hbs index c9791f0fc4..ffe69e22b7 100644 --- a/app/assets/javascripts/admin/addon/templates/web-hooks-show-events.hbs +++ b/app/assets/javascripts/admin/addon/components/webhook-events.hbs @@ -1,16 +1,17 @@ -
    - - {{d-icon "list"}} {{i18n "admin.web_hooks.events.go_list"}} - - - - {{d-icon "far-edit"}} {{i18n "admin.web_hooks.events.go_details"}} - -
    +
    + -
    - {{#if this.model}} - + {{#if this.events}} +
    {{i18n "admin.web_hooks.events.status"}}
    @@ -18,20 +19,22 @@
    {{i18n "admin.web_hooks.events.timestamp"}}
    {{i18n "admin.web_hooks.events.completion"}}
    {{i18n "admin.web_hooks.events.actions"}}
    -
    + {{#if this.hasIncoming}} {{/if}} +
      - {{#each this.model as |webHookEvent|}} - + {{#each this.events as |event|}} + {{/each}}
    - + +
    {{else}}

    {{i18n "admin.web_hooks.events.none"}}

    diff --git a/app/assets/javascripts/admin/addon/components/webhook-events.js b/app/assets/javascripts/admin/addon/components/webhook-events.js new file mode 100644 index 0000000000..5f887202e4 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/webhook-events.js @@ -0,0 +1,89 @@ +import Component from "@glimmer/component"; +import { inject as service } from "@ember/service"; +import { tracked } from "@glimmer/tracking"; +import { action } from "@ember/object"; +import { ajax } from "discourse/lib/ajax"; +import { gt, readOnly } from "@ember/object/computed"; +import { bind } from "discourse-common/utils/decorators"; +import { popupAjaxError } from "discourse/lib/ajax-error"; + +export default class WebhookEvents extends Component { + @service messageBus; + @service store; + + @tracked pingEnabled = true; + @tracked events = []; + @tracked incomingEventIds = []; + + @readOnly("incomingEventIds.length") incomingCount; + @gt("incomingCount", 0) hasIncoming; + + constructor() { + super(...arguments); + this.loadEvents(); + } + + async loadEvents() { + this.events = await this.store.findAll( + "web-hook-event", + this.args.webhookId + ); + } + + @bind + subscribe() { + const channel = `/web_hook_events/${this.args.webhookId}`; + this.messageBus.subscribe(channel, this._addIncoming); + } + + @bind + unsubscribe() { + this.messageBus.unsubscribe("/web_hook_events/*", this._addIncoming); + } + + @bind + _addIncoming(data) { + if (data.event_type === "ping") { + this.pingEnabled = true; + } + + if (!this.incomingEventIds.includes(data.web_hook_event_id)) { + this.incomingEventIds.pushObject(data.web_hook_event_id); + } + } + + @action + async showInserted(event) { + event?.preventDefault(); + + const path = `/admin/api/web_hooks/${this.args.webhookId}/events/bulk`; + const data = await ajax(path, { + data: { ids: this.incomingEventIds }, + }); + + const objects = data.map((webhookEvent) => + this.store.createRecord("web-hook-event", webhookEvent) + ); + this.events.unshiftObjects(objects); + this.incomingEventIds = []; + } + + @action + loadMore() { + this.events.loadMore(); + } + + @action + async ping() { + this.pingEnabled = false; + + try { + await ajax(`/admin/api/web_hooks/${this.args.webhookId}/ping`, { + type: "POST", + }); + } catch (error) { + this.pingEnabled = true; + popupAjaxError(error); + } + } +} diff --git a/app/assets/javascripts/admin/addon/components/webhook-status.hbs b/app/assets/javascripts/admin/addon/components/webhook-status.hbs new file mode 100644 index 0000000000..a4e3b5c7a5 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/webhook-status.hbs @@ -0,0 +1,2 @@ +{{d-icon this.iconName (hash class=this.iconClass)}} +{{this.deliveryStatus}} diff --git a/app/assets/javascripts/admin/addon/components/webhook-status.js b/app/assets/javascripts/admin/addon/components/webhook-status.js new file mode 100644 index 0000000000..258dca54cc --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/webhook-status.js @@ -0,0 +1,24 @@ +import Component from "@glimmer/component"; +import I18n from "I18n"; + +export default class WebhookStatus extends Component { + iconNames = ["far-circle", "times-circle", "circle", "circle"]; + iconClasses = ["text-muted", "text-danger", "text-successful", "text-muted"]; + + get status() { + const lastStatus = this.args.webhook.last_delivery_status; + return this.args.deliveryStatuses.find((s) => s.id === lastStatus); + } + + get deliveryStatus() { + return I18n.t(`admin.web_hooks.delivery_status.${this.status.name}`); + } + + get iconName() { + return this.iconNames[this.status.id - 1]; + } + + get iconClass() { + return this.iconClasses[this.status.id - 1]; + } +} diff --git a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-edit.js b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-edit.js new file mode 100644 index 0000000000..f24dfef939 --- /dev/null +++ b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-edit.js @@ -0,0 +1,101 @@ +import Controller, { inject as controller } from "@ember/controller"; +import EmberObject, { action } from "@ember/object"; +import I18n from "I18n"; +import { alias } from "@ember/object/computed"; +import discourseComputed from "discourse-common/utils/decorators"; +import { isEmpty } from "@ember/utils"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { inject as service } from "@ember/service"; + +export default Controller.extend({ + adminWebHooks: controller(), + dialog: service(), + eventTypes: alias("adminWebHooks.eventTypes"), + defaultEventTypes: alias("adminWebHooks.defaultEventTypes"), + contentTypes: alias("adminWebHooks.contentTypes"), + + @discourseComputed + showTagsFilter() { + return this.siteSettings.tagging_enabled; + }, + + @discourseComputed("model.isSaving", "saved", "saveButtonDisabled") + savingStatus(isSaving, saved, saveButtonDisabled) { + if (isSaving) { + return I18n.t("saving"); + } else if (!saveButtonDisabled && saved) { + return I18n.t("saved"); + } + // Use side effect of validation to clear saved text + this.set("saved", false); + return ""; + }, + + @discourseComputed("model.isNew") + saveButtonText(isNew) { + return isNew + ? I18n.t("admin.web_hooks.create") + : I18n.t("admin.web_hooks.save"); + }, + + @discourseComputed("model.secret") + secretValidation(secret) { + if (!isEmpty(secret)) { + if (secret.includes(" ")) { + return EmberObject.create({ + failed: true, + reason: I18n.t("admin.web_hooks.secret_invalid"), + }); + } + + if (secret.length < 12) { + return EmberObject.create({ + failed: true, + reason: I18n.t("admin.web_hooks.secret_too_short"), + }); + } + } + }, + + @discourseComputed("model.wildcard_web_hook", "model.web_hook_event_types.[]") + eventTypeValidation(isWildcard, eventTypes) { + if (!isWildcard && isEmpty(eventTypes)) { + return EmberObject.create({ + failed: true, + reason: I18n.t("admin.web_hooks.event_type_missing"), + }); + } + }, + + @discourseComputed( + "model.isSaving", + "secretValidation", + "eventTypeValidation", + "model.payload_url" + ) + saveButtonDisabled( + isSaving, + secretValidation, + eventTypeValidation, + payloadUrl + ) { + return isSaving + ? false + : secretValidation || eventTypeValidation || isEmpty(payloadUrl); + }, + + @action + async save() { + this.set("saved", false); + + try { + await this.model.save(); + + this.set("saved", true); + this.adminWebHooks.model.addObject(this.model); + this.transitionToRoute("adminWebHooks.show", this.model); + } catch (e) { + popupAjaxError(e); + } + }, +}); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-index.js b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-index.js new file mode 100644 index 0000000000..a1294cf09d --- /dev/null +++ b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-index.js @@ -0,0 +1,36 @@ +import Controller, { inject as controller } from "@ember/controller"; +import I18n from "I18n"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { inject as service } from "@ember/service"; +import { action } from "@ember/object"; +import { alias } from "@ember/object/computed"; + +export default Controller.extend({ + adminWebHooks: controller(), + dialog: service(), + contentTypes: alias("adminWebHooks.contentTypes"), + defaultEventTypes: alias("adminWebHooks.defaultEventTypes"), + deliveryStatuses: alias("adminWebHooks.deliveryStatuses"), + eventTypes: alias("adminWebHooks.eventTypes"), + model: alias("adminWebHooks.model"), + + @action + destroy(webhook) { + return this.dialog.deleteConfirm({ + message: I18n.t("admin.web_hooks.delete_confirm"), + didConfirm: async () => { + try { + await webhook.destroyRecord(); + this.model.removeObject(webhook); + } catch (e) { + popupAjaxError(e); + } + }, + }); + }, + + @action + loadMore() { + this.model.loadMore(); + }, +}); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show-events.js b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show-events.js deleted file mode 100644 index 84b7650ff1..0000000000 --- a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show-events.js +++ /dev/null @@ -1,83 +0,0 @@ -import Controller from "@ember/controller"; -import { ajax } from "discourse/lib/ajax"; -import { action } from "@ember/object"; -import { alias } from "@ember/object/computed"; -import discourseComputed from "discourse-common/utils/decorators"; -import { popupAjaxError } from "discourse/lib/ajax-error"; - -export default Controller.extend({ - pingDisabled: false, - incomingCount: alias("incomingEventIds.length"), - - init() { - this._super(...arguments); - - this.incomingEventIds = []; - }, - - @discourseComputed("incomingCount") - hasIncoming(incomingCount) { - return incomingCount > 0; - }, - - subscribe() { - this.messageBus.subscribe( - `/web_hook_events/${this.get("model.extras.web_hook_id")}`, - (data) => { - if (data.event_type === "ping") { - this.set("pingDisabled", false); - } - this._addIncoming(data.web_hook_event_id); - } - ); - }, - - unsubscribe() { - this.messageBus.unsubscribe("/web_hook_events/*"); - }, - - _addIncoming(eventId) { - const incomingEventIds = this.incomingEventIds; - - if (!incomingEventIds.includes(eventId)) { - incomingEventIds.pushObject(eventId); - } - }, - - @action - showInserted(event) { - event?.preventDefault(); - const webHookId = this.get("model.extras.web_hook_id"); - - ajax(`/admin/api/web_hooks/${webHookId}/events/bulk`, { - type: "GET", - data: { ids: this.incomingEventIds }, - }).then((data) => { - const objects = data.map((webHookEvent) => - this.store.createRecord("web-hook-event", webHookEvent) - ); - this.model.unshiftObjects(objects); - this.set("incomingEventIds", []); - }); - }, - - actions: { - loadMore() { - this.model.loadMore(); - }, - - ping() { - this.set("pingDisabled", true); - - ajax( - `/admin/api/web_hooks/${this.get("model.extras.web_hook_id")}/ping`, - { - type: "POST", - } - ).catch((error) => { - this.set("pingDisabled", false); - popupAjaxError(error); - }); - }, - }, -}); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show.js b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show.js index f1e8caadcf..38bef778fb 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show.js @@ -1,121 +1,32 @@ import Controller, { inject as controller } from "@ember/controller"; -import EmberObject from "@ember/object"; +import { action } from "@ember/object"; import I18n from "I18n"; -import { alias } from "@ember/object/computed"; -import discourseComputed from "discourse-common/utils/decorators"; -import { isEmpty } from "@ember/utils"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { inject as service } from "@ember/service"; export default Controller.extend({ adminWebHooks: controller(), dialog: service(), - eventTypes: alias("adminWebHooks.eventTypes"), - defaultEventTypes: alias("adminWebHooks.defaultEventTypes"), - contentTypes: alias("adminWebHooks.contentTypes"), + router: service(), - @discourseComputed - showTagsFilter() { - return this.siteSettings.tagging_enabled; + @action + edit() { + return this.router.transitionTo("adminWebHooks.edit", this.model); }, - @discourseComputed("model.isSaving", "saved", "saveButtonDisabled") - savingStatus(isSaving, saved, saveButtonDisabled) { - if (isSaving) { - return I18n.t("saving"); - } else if (!saveButtonDisabled && saved) { - return I18n.t("saved"); - } - // Use side effect of validation to clear saved text - this.set("saved", false); - return ""; - }, - - @discourseComputed("model.isNew") - saveButtonText(isNew) { - return isNew - ? I18n.t("admin.web_hooks.create") - : I18n.t("admin.web_hooks.save"); - }, - - @discourseComputed("model.secret") - secretValidation(secret) { - if (!isEmpty(secret)) { - if (secret.includes(" ")) { - return EmberObject.create({ - failed: true, - reason: I18n.t("admin.web_hooks.secret_invalid"), - }); - } - - if (secret.length < 12) { - return EmberObject.create({ - failed: true, - reason: I18n.t("admin.web_hooks.secret_too_short"), - }); - } - } - }, - - @discourseComputed("model.wildcard_web_hook", "model.web_hook_event_types.[]") - eventTypeValidation(isWildcard, eventTypes) { - if (!isWildcard && isEmpty(eventTypes)) { - return EmberObject.create({ - failed: true, - reason: I18n.t("admin.web_hooks.event_type_missing"), - }); - } - }, - - @discourseComputed( - "model.isSaving", - "secretValidation", - "eventTypeValidation", - "model.payload_url" - ) - saveButtonDisabled( - isSaving, - secretValidation, - eventTypeValidation, - payloadUrl - ) { - return isSaving - ? false - : secretValidation || eventTypeValidation || isEmpty(payloadUrl); - }, - - actions: { - save() { - this.set("saved", false); - const model = this.model; - const isNew = model.get("isNew"); - - return model - .save() - .then(() => { - this.set("saved", true); - this.adminWebHooks.get("model").addObject(model); - - if (isNew) { - this.transitionToRoute("adminWebHooks.show", model.get("id")); - } - }) - .catch(popupAjaxError); - }, - - destroy() { - return this.dialog.yesNoConfirm({ - message: I18n.t("admin.web_hooks.delete_confirm"), - didConfirm: () => { - this.model - .destroyRecord() - .then(() => { - this.adminWebHooks.get("model").removeObject(this.model); - this.transitionToRoute("adminWebHooks"); - }) - .catch(popupAjaxError); - }, - }); - }, + @action + destroy() { + return this.dialog.deleteConfirm({ + message: I18n.t("admin.web_hooks.delete_confirm"), + didConfirm: async () => { + try { + await this.model.destroyRecord(); + this.adminWebHooks.model.removeObject(this.model); + this.transitionToRoute("adminWebHooks"); + } catch (e) { + popupAjaxError(e); + } + }, + }); }, }); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks.js b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks.js index 405f859413..fa4ba1ee7d 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks.js @@ -1,29 +1,3 @@ import Controller from "@ember/controller"; -import I18n from "I18n"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { inject as service } from "@ember/service"; -import { action } from "@ember/object"; -export default Controller.extend({ - dialog: service(), - - @action - destroy(webhook) { - return this.dialog.yesNoConfirm({ - message: I18n.t("admin.web_hooks.delete_confirm"), - didConfirm: () => { - webhook - .destroyRecord() - .then(() => { - this.model.removeObject(webhook); - }) - .catch(popupAjaxError); - }, - }); - }, - - @action - loadMore() { - this.model.loadMore(); - }, -}); +export default Controller.extend({}); diff --git a/app/assets/javascripts/admin/addon/controllers/modals/admin-merge-users-progress.js b/app/assets/javascripts/admin/addon/controllers/modals/admin-merge-users-progress.js index 3d9248d36d..bdbcf2d220 100644 --- a/app/assets/javascripts/admin/addon/controllers/modals/admin-merge-users-progress.js +++ b/app/assets/javascripts/admin/addon/controllers/modals/admin-merge-users-progress.js @@ -2,30 +2,33 @@ import Controller from "@ember/controller"; import DiscourseURL from "discourse/lib/url"; import I18n from "I18n"; import ModalFunctionality from "discourse/mixins/modal-functionality"; -import messageBus from "message-bus-client"; +import { bind } from "discourse-common/utils/decorators"; export default Controller.extend(ModalFunctionality, { message: I18n.t("admin.user.merging_user"), onShow() { - messageBus.subscribe("/merge_user", (data) => { - if (data.merged) { - if (/^\/admin\/users\/list\//.test(location)) { - DiscourseURL.redirectTo(location); - } else { - DiscourseURL.redirectTo( - `/admin/users/${data.user.id}/${data.user.username}` - ); - } - } else if (data.message) { - this.set("message", data.message); - } else if (data.failed) { - this.set("message", I18n.t("admin.user.merge_failed")); - } - }); + this.messageBus.subscribe("/merge_user", this.onMessage); }, onClose() { - this.messageBus.unsubscribe("/merge_user"); + this.messageBus.unsubscribe("/merge_user", this.onMessage); + }, + + @bind + onMessage(data) { + if (data.merged) { + if (/^\/admin\/users\/list\//.test(location)) { + DiscourseURL.redirectTo(location); + } else { + DiscourseURL.redirectTo( + `/admin/users/${data.user.id}/${data.user.username}` + ); + } + } else if (data.message) { + this.set("message", data.message); + } else if (data.failed) { + this.set("message", I18n.t("admin.user.merge_failed")); + } }, }); diff --git a/app/assets/javascripts/admin/addon/controllers/modals/admin-silence-user.js b/app/assets/javascripts/admin/addon/controllers/modals/admin-silence-user.js index 506598625d..077a086330 100644 --- a/app/assets/javascripts/admin/addon/controllers/modals/admin-silence-user.js +++ b/app/assets/javascripts/admin/addon/controllers/modals/admin-silence-user.js @@ -9,7 +9,11 @@ export default Controller.extend(PenaltyController, { onShow() { this.resetModal(); - this.setProperties({ silenceUntil: null, silencing: false }); + this.setProperties({ + silenceUntil: null, + silencing: false, + otherUserIds: [], + }); }, finishedSetup() { @@ -36,6 +40,7 @@ export default Controller.extend(PenaltyController, { post_id: this.postId, post_action: this.postAction, post_edit: this.postEdit, + other_user_ids: this.otherUserIds, }); }).finally(() => this.set("silencing", false)); }, diff --git a/app/assets/javascripts/admin/addon/controllers/modals/admin-suspend-user.js b/app/assets/javascripts/admin/addon/controllers/modals/admin-suspend-user.js index c82a562808..c9f34d8957 100644 --- a/app/assets/javascripts/admin/addon/controllers/modals/admin-suspend-user.js +++ b/app/assets/javascripts/admin/addon/controllers/modals/admin-suspend-user.js @@ -9,7 +9,11 @@ export default Controller.extend(PenaltyController, { onShow() { this.resetModal(); - this.setProperties({ suspendUntil: null, suspending: false }); + this.setProperties({ + suspendUntil: null, + suspending: false, + otherUserIds: [], + }); }, finishedSetup() { @@ -28,7 +32,6 @@ export default Controller.extend(PenaltyController, { } this.set("suspending", true); - this.penalize(() => { return this.user.suspend({ suspend_until: this.suspendUntil, @@ -37,6 +40,7 @@ export default Controller.extend(PenaltyController, { post_id: this.postId, post_action: this.postAction, post_edit: this.postEdit, + other_user_ids: this.otherUserIds, }); }).finally(() => this.set("suspending", false)); }, diff --git a/app/assets/javascripts/admin/addon/models/web-hook.js b/app/assets/javascripts/admin/addon/models/web-hook.js index 1b74d50b7b..500311bec8 100644 --- a/app/assets/javascripts/admin/addon/models/web-hook.js +++ b/app/assets/javascripts/admin/addon/models/web-hook.js @@ -15,7 +15,7 @@ export default RestModel.extend({ groupsFilterInName: null, @discourseComputed("wildcard_web_hook") - webHookType: { + webhookType: { get(wildcard) { return wildcard ? "wildcard" : "individual"; }, diff --git a/app/assets/javascripts/admin/addon/routes/admin-backups-index.js b/app/assets/javascripts/admin/addon/routes/admin-backups-index.js index cb05c26580..1f7ea93a69 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-backups-index.js +++ b/app/assets/javascripts/admin/addon/routes/admin-backups-index.js @@ -1,14 +1,14 @@ import Backup from "admin/models/backup"; import Route from "@ember/routing/route"; +import { bind } from "discourse-common/utils/decorators"; export default Route.extend({ activate() { - this.messageBus.subscribe("/admin/backups", (backups) => - this.controller.set( - "model", - backups.map((backup) => Backup.create(backup)) - ) - ); + this.messageBus.subscribe("/admin/backups", this.onMessage); + }, + + deactivate() { + this.messageBus.unsubscribe("/admin/backups", this.onMessage); }, model() { @@ -17,7 +17,11 @@ export default Route.extend({ ); }, - deactivate() { - this.messageBus.unsubscribe("/admin/backups"); + @bind + onMessage(backups) { + this.controller.set( + "model", + backups.map((backup) => Backup.create(backup)) + ); }, }); diff --git a/app/assets/javascripts/admin/addon/routes/admin-backups.js b/app/assets/javascripts/admin/addon/routes/admin-backups.js index c78ef4ca1c..1379a09754 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-backups.js +++ b/app/assets/javascripts/admin/addon/routes/admin-backups.js @@ -10,46 +10,19 @@ import { extractError } from "discourse/lib/ajax-error"; import getURL from "discourse-common/lib/get-url"; import showModal from "discourse/lib/show-modal"; import { inject as service } from "@ember/service"; +import { bind } from "discourse-common/utils/decorators"; + const LOG_CHANNEL = "/admin/backups/logs"; export default DiscourseRoute.extend({ dialog: service(), activate() { - this.messageBus.subscribe(LOG_CHANNEL, (log) => { - if (log.message === "[STARTED]") { - User.currentProp("hideReadOnlyAlert", true); - this.controllerFor("adminBackups").set( - "model.isOperationRunning", - true - ); - this.controllerFor("adminBackupsLogs").get("logs").clear(); - } else if (log.message === "[FAILED]") { - this.controllerFor("adminBackups").set( - "model.isOperationRunning", - false - ); - this.dialog.alert( - I18n.t("admin.backups.operations.failed", { - operation: log.operation, - }) - ); - } else if (log.message === "[SUCCESS]") { - User.currentProp("hideReadOnlyAlert", false); - this.controllerFor("adminBackups").set( - "model.isOperationRunning", - false - ); - if (log.operation === "restore") { - // redirect to homepage when the restore is done (session might be lost) - window.location = getURL("/"); - } - } else { - this.controllerFor("adminBackupsLogs") - .get("logs") - .pushObject(EmberObject.create(log)); - } - }); + this.messageBus.subscribe(LOG_CHANNEL, this.onMessage); + }, + + deactivate() { + this.messageBus.unsubscribe(LOG_CHANNEL, this.onMessage); }, model() { @@ -64,8 +37,31 @@ export default DiscourseRoute.extend({ ); }, - deactivate() { - this.messageBus.unsubscribe(LOG_CHANNEL); + @bind + onMessage(log) { + if (log.message === "[STARTED]") { + User.currentProp("hideReadOnlyAlert", true); + this.controllerFor("adminBackups").set("model.isOperationRunning", true); + this.controllerFor("adminBackupsLogs").get("logs").clear(); + } else if (log.message === "[FAILED]") { + this.controllerFor("adminBackups").set("model.isOperationRunning", false); + this.dialog.alert( + I18n.t("admin.backups.operations.failed", { + operation: log.operation, + }) + ); + } else if (log.message === "[SUCCESS]") { + User.currentProp("hideReadOnlyAlert", false); + this.controllerFor("adminBackups").set("model.isOperationRunning", false); + if (log.operation === "restore") { + // redirect to homepage when the restore is done (session might be lost) + window.location = getURL("/"); + } + } else { + this.controllerFor("adminBackupsLogs") + .get("logs") + .pushObject(EmberObject.create(log)); + } }, actions: { diff --git a/app/assets/javascripts/admin/addon/routes/admin-route-map.js b/app/assets/javascripts/admin/addon/routes/admin-route-map.js index 9b888f9ce5..37272c1c07 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-route-map.js +++ b/app/assets/javascripts/admin/addon/routes/admin-route-map.js @@ -123,7 +123,7 @@ export default function () { { path: "/web_hooks", resetNamespace: true }, function () { this.route("show", { path: "/:web_hook_id" }); - this.route("showEvents", { path: "/:web_hook_id/events" }); + this.route("edit", { path: "/:web_hook_id/edit" }); } ); }); diff --git a/app/assets/javascripts/admin/addon/routes/admin-web-hooks-edit.js b/app/assets/javascripts/admin/addon/routes/admin-web-hooks-edit.js new file mode 100644 index 0000000000..6bd96b117b --- /dev/null +++ b/app/assets/javascripts/admin/addon/routes/admin-web-hooks-edit.js @@ -0,0 +1,28 @@ +import DiscourseRoute from "discourse/routes/discourse"; + +export default DiscourseRoute.extend({ + serialize(model) { + return { web_hook_id: model.id || "new" }; + }, + + model(params) { + if (params.web_hook_id === "new") { + return this.store.createRecord("web-hook"); + } + + return this.store.find("web-hook", params.web_hook_id); + }, + + setupController(controller, model) { + this._super(...arguments); + + if (model.get("isNew")) { + model.set( + "web_hook_event_types", + this.controllerFor("adminWebHooks").defaultEventTypes + ); + } + + controller.set("saved", false); + }, +}); diff --git a/app/assets/javascripts/admin/addon/routes/admin-web-hooks-show-events.js b/app/assets/javascripts/admin/addon/routes/admin-web-hooks-show-events.js deleted file mode 100644 index 1b7923224b..0000000000 --- a/app/assets/javascripts/admin/addon/routes/admin-web-hooks-show-events.js +++ /dev/null @@ -1,21 +0,0 @@ -import DiscourseRoute from "discourse/routes/discourse"; -import { get } from "@ember/object"; - -export default DiscourseRoute.extend({ - model(params) { - return this.store.findAll("web-hook-event", get(params, "web_hook_id")); - }, - - setupController(controller, model) { - controller.set("model", model); - controller.subscribe(); - }, - - deactivate() { - this.controllerFor("adminWebHooks.showEvents").unsubscribe(); - }, - - renderTemplate() { - this.render("admin/templates/web-hooks-show-events", { into: "adminApi" }); - }, -}); diff --git a/app/assets/javascripts/admin/addon/routes/admin-web-hooks-show.js b/app/assets/javascripts/admin/addon/routes/admin-web-hooks-show.js index d76be7f5df..369bbd0059 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-web-hooks-show.js +++ b/app/assets/javascripts/admin/addon/routes/admin-web-hooks-show.js @@ -1,26 +1,7 @@ import DiscourseRoute from "discourse/routes/discourse"; -import { get } from "@ember/object"; export default DiscourseRoute.extend({ - serialize(model) { - return { web_hook_id: model.get("id") || "new" }; - }, - model(params) { - if (params.web_hook_id === "new") { - return this.store.createRecord("web-hook"); - } - return this.store.find("web-hook", get(params, "web_hook_id")); - }, - - setupController(controller, model) { - if (model.get("isNew")) { - model.set("web_hook_event_types", controller.get("defaultEventTypes")); - } - - model.set("category_ids", model.get("category_ids")); - model.set("tag_names", model.get("tag_names")); - model.set("group_ids", model.get("group_ids")); - controller.setProperties({ model, saved: false }); + return this.store.find("web-hook", params.web_hook_id); }, }); diff --git a/app/assets/javascripts/admin/addon/routes/admin-web-hooks.js b/app/assets/javascripts/admin/addon/routes/admin-web-hooks.js index 4b20731136..3619142a4c 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-web-hooks.js +++ b/app/assets/javascripts/admin/addon/routes/admin-web-hooks.js @@ -1,4 +1,5 @@ import Route from "@ember/routing/route"; + export default Route.extend({ model() { return this.store.findAll("web-hook"); diff --git a/app/assets/javascripts/admin/addon/templates/components/admin-penalty-similar-users.hbs b/app/assets/javascripts/admin/addon/templates/components/admin-penalty-similar-users.hbs new file mode 100644 index 0000000000..ccb35da4d8 --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/components/admin-penalty-similar-users.hbs @@ -0,0 +1,35 @@ +
    +

    + {{i18n "admin.user.other_matches" (hash count=this.user.similar_users_count username=this.user.username)}} +

    + + + + + + + + + + + + + + + + {{#each this.user.similar_users as |user|}} + + + + + + + + + + {{/each}} + +
    {{i18n "username"}}{{i18n "last_seen"}}{{i18n "admin.user.topics_entered"}}{{i18n "admin.user.posts_read_count"}}{{i18n "admin.user.time_read"}}{{i18n "created"}}
    + + {{avatar user imageSize="small"}} {{user.username}}{{format-duration user.last_seen_age}}{{number user.topics_entered}}{{number user.posts_read_count}}{{format-duration user.time_read}}{{format-duration user.created_at_age}}
    +
    diff --git a/app/assets/javascripts/admin/addon/templates/components/admin-web-hook-event-chooser.hbs b/app/assets/javascripts/admin/addon/templates/components/admin-web-hook-event-chooser.hbs deleted file mode 100644 index 68f9a2ae9f..0000000000 --- a/app/assets/javascripts/admin/addon/templates/components/admin-web-hook-event-chooser.hbs +++ /dev/null @@ -1,3 +0,0 @@ - - -

    {{this.details}}

    diff --git a/app/assets/javascripts/admin/addon/templates/components/admin-web-hook-event.hbs b/app/assets/javascripts/admin/addon/templates/components/admin-web-hook-event.hbs deleted file mode 100644 index 7e61c6b327..0000000000 --- a/app/assets/javascripts/admin/addon/templates/components/admin-web-hook-event.hbs +++ /dev/null @@ -1,19 +0,0 @@ -
    - {{this.model.status}} -
    -
    {{this.model.id}}
    -
    {{this.createdAt}}
    -
    {{this.completion}}
    -
    - - - -
    -{{#if this.expandDetails}} -
    -

    {{i18n "admin.web_hooks.events.headers"}}

    -
    {{this.headers}}
    -

    {{this.bodyLabel}}

    -
    {{this.body}}
    -
    -{{/if}} diff --git a/app/assets/javascripts/admin/addon/templates/components/admin-web-hook-status.hbs b/app/assets/javascripts/admin/addon/templates/components/admin-web-hook-status.hbs deleted file mode 100644 index 0f7a1516b7..0000000000 --- a/app/assets/javascripts/admin/addon/templates/components/admin-web-hook-status.hbs +++ /dev/null @@ -1 +0,0 @@ -{{this.circleIcon}} {{this.deliveryStatus}} diff --git a/app/assets/javascripts/admin/addon/templates/customize-themes-show.hbs b/app/assets/javascripts/admin/addon/templates/customize-themes-show.hbs index 4fa8ebe52d..d4a62121f7 100644 --- a/app/assets/javascripts/admin/addon/templates/customize-themes-show.hbs +++ b/app/assets/javascripts/admin/addon/templates/customize-themes-show.hbs @@ -173,11 +173,12 @@ {{/if}} {{#unless this.model.component}} - +
    {{i18n "admin.customize.theme.color_scheme"}}
    +
    {{i18n "admin.customize.theme.color_scheme_select"}}
    +
    {{#if this.colorSchemeChanged}} diff --git a/app/assets/javascripts/admin/addon/templates/logs/screened-ip-addresses.hbs b/app/assets/javascripts/admin/addon/templates/logs/screened-ip-addresses.hbs index ab20b2316b..84161e2bbe 100644 --- a/app/assets/javascripts/admin/addon/templates/logs/screened-ip-addresses.hbs +++ b/app/assets/javascripts/admin/addon/templates/logs/screened-ip-addresses.hbs @@ -54,10 +54,8 @@ {{#if item.last_match_at}} -
    - {{i18n "admin.logs.last_match_at"}} - {{age-with-tooltip item.last_match_at}} -
    +
    {{i18n "admin.logs.last_match_at"}}
    + {{age-with-tooltip item.last_match_at}} {{/if}} diff --git a/app/assets/javascripts/admin/addon/templates/modal/admin-silence-user.hbs b/app/assets/javascripts/admin/addon/templates/modal/admin-silence-user.hbs index 08f3e0a909..374cd5a7a8 100644 --- a/app/assets/javascripts/admin/addon/templates/modal/admin-silence-user.hbs +++ b/app/assets/javascripts/admin/addon/templates/modal/admin-silence-user.hbs @@ -18,6 +18,10 @@ {{/if}} + {{#if this.user.similar_users}} + + {{/if}} + diff --git a/app/assets/javascripts/admin/addon/templates/modal/admin-suspend-user.hbs b/app/assets/javascripts/admin/addon/templates/modal/admin-suspend-user.hbs index 71daba820c..2aa787a65c 100644 --- a/app/assets/javascripts/admin/addon/templates/modal/admin-suspend-user.hbs +++ b/app/assets/javascripts/admin/addon/templates/modal/admin-suspend-user.hbs @@ -13,12 +13,15 @@
    - + {{#if this.postId}} {{/if}} + {{#if this.user.similar_users}} + + {{/if}} {{else}}
    {{i18n "admin.user.cant_suspend"}} diff --git a/app/assets/javascripts/admin/addon/templates/web-hooks-edit.hbs b/app/assets/javascripts/admin/addon/templates/web-hooks-edit.hbs new file mode 100644 index 0000000000..60139980d8 --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/web-hooks-edit.hbs @@ -0,0 +1,122 @@ + + {{d-icon "arrow-left"}} + {{i18n "admin.web_hooks.go_back"}} + + +
    +

    {{i18n "admin.web_hooks.detailed_instruction"}}

    + +
    +
    + + + +
    + +
    + + +
    + +
    + + + +
    + +
    + + + + + {{#unless this.model.wildcard_web_hook}} +
    + {{#each this.eventTypes as |type|}} + + {{/each}} +
    + {{/unless}} + + +
    + +
    +
    + + +
    {{i18n "admin.web_hooks.categories_filter_instructions"}}
    +
    + + {{#if this.showTagsFilter}} +
    + + +
    {{i18n "admin.web_hooks.tags_filter_instructions"}}
    +
    + {{/if}} + +
    + + +
    {{i18n "admin.web_hooks.groups_filter_instructions"}}
    +
    +
    + + + + + +
    + + + {{#if this.model.active}} +
    {{i18n "admin.web_hooks.active_notice"}}
    + {{/if}} +
    + + +
    + + + {{#if this.model.isNew}} + + {{i18n "cancel"}} + + {{else}} + + {{i18n "cancel"}} + + {{/if}} + + {{this.savingStatus}} +
    +
    diff --git a/app/assets/javascripts/admin/addon/templates/web-hooks-index.hbs b/app/assets/javascripts/admin/addon/templates/web-hooks-index.hbs new file mode 100644 index 0000000000..6587ea16c1 --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/web-hooks-index.hbs @@ -0,0 +1,70 @@ +
    +

    {{i18n "admin.web_hooks.instruction"}}

    + +
    + + {{d-icon "plus"}} + {{i18n "admin.web_hooks.new"}} + +
    + + {{#if this.model}} + + + + + + + + + + + + {{#each this.model as |webhook|}} + + + + + + + {{/each}} + +
    {{i18n "admin.web_hooks.delivery_status.title"}}{{i18n "admin.web_hooks.payload_url"}}{{i18n "admin.web_hooks.description_label"}}{{i18n "admin.web_hooks.controls"}}
    + + + + + + {{webhook.payload_url}} + + {{webhook.description}} + + {{d-icon "far-edit"}} + + + +
    + +
    + {{else}} +

    {{i18n "admin.web_hooks.none"}}

    + {{/if}} +
    diff --git a/app/assets/javascripts/admin/addon/templates/web-hooks-show.hbs b/app/assets/javascripts/admin/addon/templates/web-hooks-show.hbs index 2ce340bc71..91f3523d5b 100644 --- a/app/assets/javascripts/admin/addon/templates/web-hooks-show.hbs +++ b/app/assets/javascripts/admin/addon/templates/web-hooks-show.hbs @@ -3,90 +3,32 @@ {{i18n "admin.web_hooks.go_back"}} -
    -

    {{i18n "admin.web_hooks.detailed_instruction"}}

    -
    -
    - - - -
    +
    +

    + {{this.model.payload_url}} -
    - - -
    + -
    - - - -
    + +

    -
    - -
    - - {{i18n "admin.web_hooks.individual_event"}} - -
    - {{#unless this.model.wildcard_web_hook}} -
    - {{#each this.eventTypes as |type|}} - - {{/each}} -
    - {{/unless}} -
    - - {{i18n "admin.web_hooks.wildcard_event"}} -
    -
    +
    + + {{i18n "admin.web_hooks.description_label"}}: + -
    -
    - - -
    {{i18n "admin.web_hooks.categories_filter_instructions"}}
    -
    - {{#if this.showTagsFilter}} -
    - - -
    {{i18n "admin.web_hooks.tags_filter_instructions"}}
    -
    - {{/if}} -
    - - -
    {{i18n "admin.web_hooks.groups_filter_instructions"}}
    -
    -
    - - - -
    - {{i18n "admin.web_hooks.verify_certificate"}} -
    -
    -
    - {{i18n "admin.web_hooks.active"}} -
    - {{#if this.model.active}} -
    {{i18n "admin.web_hooks.active_notice"}}
    - {{/if}} -
    - - -
    - - - {{#unless this.model.isNew}} - - - {{i18n "admin.web_hooks.events.go_events"}} - - {{/unless}} - {{this.savingStatus}} + {{this.model.description}}
    + + diff --git a/app/assets/javascripts/admin/addon/templates/web-hooks.hbs b/app/assets/javascripts/admin/addon/templates/web-hooks.hbs index ecf04bb08f..c24cd68950 100644 --- a/app/assets/javascripts/admin/addon/templates/web-hooks.hbs +++ b/app/assets/javascripts/admin/addon/templates/web-hooks.hbs @@ -1,38 +1 @@ -
    -

    {{i18n "admin.web_hooks.instruction"}}

    -
    - - {{d-icon "plus"}} {{i18n "admin.web_hooks.new"}} - -
    - {{#if this.model}} - - - - - - - - - - - - {{#each this.model as |webHook|}} - - - - - - - {{/each}} - -
    {{i18n "admin.web_hooks.delivery_status.title"}}{{i18n "admin.web_hooks.payload_url"}}{{i18n "admin.web_hooks.description"}}{{i18n "admin.web_hooks.controls"}}
    {{webHook.payload_url}}{{webHook.description}} - {{d-icon "far-edit"}} - -
    - -
    - {{else}} -

    {{i18n "admin.web_hooks.none"}}

    - {{/if}} -
    +{{outlet}} diff --git a/app/assets/javascripts/bootstrap-json/index.js b/app/assets/javascripts/bootstrap-json/index.js index 44414a9584..a2087fdc16 100644 --- a/app/assets/javascripts/bootstrap-json/index.js +++ b/app/assets/javascripts/bootstrap-json/index.js @@ -319,7 +319,13 @@ async function handleRequest(proxy, baseURL, req, res) { }); response.headers.forEach((value, header) => { - res.set(header, value); + if (header === "set-cookie") { + // Special handling to get array of multiple Set-Cookie header values + // per https://github.com/node-fetch/node-fetch/issues/251#issuecomment-428143940 + res.set("set-cookie", response.headers.raw()["set-cookie"]); + } else { + res.set(header, value); + } }); res.set("content-encoding", null); diff --git a/app/assets/javascripts/dialog-holder/addon/components/dialog-holder.hbs b/app/assets/javascripts/dialog-holder/addon/components/dialog-holder.hbs index 7a1729f5b1..457b9b2652 100644 --- a/app/assets/javascripts/dialog-holder/addon/components/dialog-holder.hbs +++ b/app/assets/javascripts/dialog-holder/addon/components/dialog-holder.hbs @@ -10,9 +10,14 @@
    {{/if}} - {{#if this.dialog.message}} + {{#if (or this.dialog.message this.dialog.confirmPhrase)}}
    - {{this.dialog.message}} + {{#if this.dialog.message}} +

    {{this.dialog.message}}

    + {{/if}} + {{#if this.dialog.confirmPhrase}} + + {{/if}}
    {{/if}} @@ -21,9 +26,9 @@ {{#each this.dialog.buttons as |button|}} {{else}} - + {{#if this.dialog.shouldDisplayCancel}} - + {{/if}} {{/each}}
    diff --git a/app/assets/javascripts/dialog-holder/addon/services/dialog.js b/app/assets/javascripts/dialog-holder/addon/services/dialog.js index 824e6e02d3..3fed408075 100644 --- a/app/assets/javascripts/dialog-holder/addon/services/dialog.js +++ b/app/assets/javascripts/dialog-holder/addon/services/dialog.js @@ -1,6 +1,7 @@ import Service from "@ember/service"; import A11yDialog from "a11y-dialog"; import { bind } from "discourse-common/utils/decorators"; +import { isBlank } from "@ember/utils"; export default Service.extend({ message: null, @@ -13,7 +14,10 @@ export default Service.extend({ confirmButtonIcon: null, confirmButtonLabel: null, confirmButtonClass: null, + confirmPhrase: null, + confirmPhraseInput: null, cancelButtonLabel: null, + cancelButtonClass: null, shouldDisplayCancel: null, didConfirm: null, @@ -32,6 +36,8 @@ export default Service.extend({ confirmButtonLabel = "ok_value", confirmButtonClass = "btn-primary", cancelButtonLabel = "cancel_value", + cancelButtonClass = "btn-default", + confirmPhrase, shouldDisplayCancel, didConfirm, @@ -39,6 +45,8 @@ export default Service.extend({ buttons, } = params; + let confirmButtonDisabled = !isBlank(confirmPhrase); + const element = document.getElementById("dialog-holder"); this.setProperties({ @@ -49,10 +57,13 @@ export default Service.extend({ title, titleElementId: title !== null ? "dialog-title" : null, + confirmButtonDisabled, confirmButtonClass, confirmButtonLabel, confirmButtonIcon, + confirmPhrase, cancelButtonLabel, + cancelButtonClass, shouldDisplayCancel, didConfirm, @@ -131,7 +142,10 @@ export default Service.extend({ confirmButtonLabel: null, confirmButtonIcon: null, cancelButtonLabel: null, + cancelButtonClass: null, shouldDisplayCancel: null, + confirmPhrase: null, + confirmPhraseInput: null, didConfirm: null, didCancel: null, @@ -160,4 +174,12 @@ export default Service.extend({ cancel() { this.dialogInstance.hide(); }, + + @bind + onConfirmPhraseInput() { + this.set( + "confirmButtonDisabled", + this.confirmPhrase && this.confirmPhraseInput !== this.confirmPhrase + ); + }, }); diff --git a/app/assets/javascripts/discourse-common/addon/lib/icon-library.js b/app/assets/javascripts/discourse-common/addon/lib/icon-library.js index df1149d8a5..8882d7c8d6 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/icon-library.js +++ b/app/assets/javascripts/discourse-common/addon/lib/icon-library.js @@ -65,16 +65,19 @@ export function enableMissingIconWarning() { } export function renderIcon(renderType, id, params) { - for (let i = 0; i < _renderers.length; i++) { - let renderer = _renderers[i]; - let rendererForType = renderer[renderType]; + params ||= {}; - if (rendererForType) { - const icon = { id, replacementId: REPLACEMENTS[id] }; - let result = rendererForType(icon, params || {}); - if (result) { - return result; - } + for (const renderer of _renderers) { + const rendererForType = renderer[renderType]; + if (!rendererForType) { + continue; + } + + const icon = { id, replacementId: REPLACEMENTS[id] }; + const result = rendererForType(icon, params); + + if (result) { + return result; } } } diff --git a/app/assets/javascripts/discourse-common/package.json b/app/assets/javascripts/discourse-common/package.json index b9cff5c11a..74f789f3f4 100644 --- a/app/assets/javascripts/discourse-common/package.json +++ b/app/assets/javascripts/discourse-common/package.json @@ -15,12 +15,12 @@ "start": "ember serve" }, "dependencies": { - "@uppy/aws-s3": "^2.2.1", - "@uppy/aws-s3-multipart": "^2.4.1", - "@uppy/core": "^2.3.1", - "@uppy/drop-target": "^1.1.3", - "@uppy/utils": "^4.1.0", - "@uppy/xhr-upload": "^2.1.2", + "@uppy/aws-s3": "^3.0.4", + "@uppy/aws-s3-multipart": "^3.1.1", + "@uppy/core": "^3.0.4", + "@uppy/drop-target": "^2.0.1", + "@uppy/utils": "^5.1.1", + "@uppy/xhr-upload": "^3.0.4", "ember-auto-import": "^2.5.0", "ember-cli-babel": "^7.26.10", "ember-cli-htmlbars": "^6.1.1", diff --git a/app/assets/javascripts/discourse-plugins/colocated-template-compiler.js b/app/assets/javascripts/discourse-plugins/colocated-template-compiler.js index 9cc4d462b2..04611e3697 100644 --- a/app/assets/javascripts/discourse-plugins/colocated-template-compiler.js +++ b/app/assets/javascripts/discourse-plugins/colocated-template-compiler.js @@ -3,12 +3,12 @@ const ColocatedTemplateProcessor = require("ember-cli-htmlbars/lib/colocated-bro module.exports = class DiscoursePluginColocatedTemplateProcessor extends ( ColocatedTemplateProcessor ) { - constructor(tree, discoursePluginName) { + constructor(tree, rootName) { super(tree); - this.discoursePluginName = discoursePluginName; + this.rootName = rootName; } detectRootName() { - return `discourse/plugins/${this.discoursePluginName}/discourse`; + return this.rootName; } }; diff --git a/app/assets/javascripts/discourse-plugins/index.js b/app/assets/javascripts/discourse-plugins/index.js index 730f926472..b4b25cfcc3 100644 --- a/app/assets/javascripts/discourse-plugins/index.js +++ b/app/assets/javascripts/discourse-plugins/index.js @@ -176,7 +176,15 @@ module.exports = { tree = RawHandlebarsCompiler(tree); - tree = new DiscoursePluginColocatedTemplateProcessor(tree, pluginName); + const colocateBase = `discourse/plugins/${pluginName}`; + tree = new DiscoursePluginColocatedTemplateProcessor( + tree, + `${colocateBase}/discourse` + ); + tree = new DiscoursePluginColocatedTemplateProcessor( + tree, + `${colocateBase}/admin` + ); tree = this.compileTemplates(tree); tree = this.processedAddonJsFiles(tree); diff --git a/app/assets/javascripts/discourse/app/components/basic-topic-list.js b/app/assets/javascripts/discourse/app/components/basic-topic-list.js index ca8fae3318..b7839ddf47 100644 --- a/app/assets/javascripts/discourse/app/components/basic-topic-list.js +++ b/app/assets/javascripts/discourse/app/components/basic-topic-list.js @@ -1,5 +1,8 @@ import { alias, not } from "@ember/object/computed"; -import discourseComputed, { observes } from "discourse-common/utils/decorators"; +import discourseComputed, { + bind, + observes, +} from "discourse-common/utils/decorators"; import Component from "@ember/component"; export default Component.extend({ @@ -40,18 +43,11 @@ export default Component.extend({ this._super(...arguments); this.topics.forEach((topic) => { - const includeUnreadIndicator = - typeof topic.unread_by_group_member !== "undefined"; - - if (includeUnreadIndicator) { - const unreadIndicatorChannel = `/private-messages/unread-indicator/${topic.id}`; - this.messageBus.subscribe(unreadIndicatorChannel, (data) => { - const nodeClassList = document.querySelector( - `.indicator-topic-${data.topic_id}` - ).classList; - - nodeClassList.toggle("read", !data.show_indicator); - }); + if (typeof topic.unread_by_group_member !== "undefined") { + this.messageBus.subscribe( + `/private-messages/unread-indicator/${topic.id}`, + this.onMessage + ); } }); }, @@ -59,15 +55,19 @@ export default Component.extend({ willDestroyElement() { this._super(...arguments); - this.topics.forEach((topic) => { - const includeUnreadIndicator = - typeof topic.unread_by_group_member !== "undefined"; + this.messageBus.unsubscribe( + "/private-messages/unread-indicator/*", + this.onMessage + ); + }, - if (includeUnreadIndicator) { - const unreadIndicatorChannel = `/private-messages/unread-indicator/${topic.id}`; - this.messageBus.unsubscribe(unreadIndicatorChannel); - } - }); + @bind + onMessage(data) { + const nodeClassList = document.querySelector( + `.indicator-topic-${data.topic_id}` + ).classList; + + nodeClassList.toggle("read", !data.show_indicator); }, @discourseComputed("topics") diff --git a/app/assets/javascripts/discourse/app/components/bookmark-icon.js b/app/assets/javascripts/discourse/app/components/bookmark-icon.js index ca18e28607..3855b471a2 100644 --- a/app/assets/javascripts/discourse/app/components/bookmark-icon.js +++ b/app/assets/javascripts/discourse/app/components/bookmark-icon.js @@ -41,7 +41,7 @@ export default class BookmarkIcon extends Component { if (!isEmpty(this.bookmark.reminder_at)) { const formattedTime = formattedReminderTime( this.bookmark.reminder_at, - this.currentUser.timezone + this.currentUser.user_option.timezone ); return I18n.t("bookmarks.created_with_reminder_generic", { date: formattedTime, diff --git a/app/assets/javascripts/discourse/app/components/bookmark.js b/app/assets/javascripts/discourse/app/components/bookmark.js index 879ef027fc..811f33e793 100644 --- a/app/assets/javascripts/discourse/app/components/bookmark.js +++ b/app/assets/javascripts/discourse/app/components/bookmark.js @@ -57,7 +57,7 @@ export default Component.extend({ postDetectedLocalTime: null, postDetectedLocalTimezone: null, prefilledDatetime: null, - userTimezone: this.currentUser.timezone, + userTimezone: this.currentUser.user_option.timezone, showOptions: false, _itsatrap: new ItsATrap(), autoDeletePreference: this.model.autoDeletePreference || 0, @@ -154,7 +154,7 @@ export default Component.extend({ } this.currentUser.set( - "bookmark_auto_delete_preference", + "user_option.bookmark_auto_delete_preference", this.autoDeletePreference ); diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js index f08549f397..2c8cd34338 100644 --- a/app/assets/javascripts/discourse/app/components/composer-editor.js +++ b/app/assets/javascripts/discourse/app/components/composer-editor.js @@ -23,7 +23,6 @@ import { linkSeenHashtagsInContext, } from "discourse/lib/hashtag-autocomplete"; import { - cannotSee, fetchUnseenMentions, linkSeenMentions, } from "discourse/lib/link-mentions"; @@ -126,6 +125,7 @@ export default Component.extend(ComposerUploadUppy, { init() { this._super(...arguments); this.warnedCannotSeeMentions = []; + this.warnedGroupMentions = []; }, @discourseComputed("composer.requiredCategoryMissing") @@ -474,13 +474,15 @@ export default Component.extend(ComposerUploadUppy, { }, _renderUnseenMentions(preview, unseen) { - // 'Create a New Topic' scenario is not supported (per conversation with codinghorror) - // https://meta.discourse.org/t/taking-another-1-7-release-task/51986/7 - fetchUnseenMentions(unseen, this.get("composer.topic.id")).then((r) => { + fetchUnseenMentions({ + names: unseen, + topicId: this.get("composer.topic.id"), + allowedNames: this.get("composer.targetRecipients")?.split(","), + }).then((response) => { linkSeenMentions(preview, this.siteSettings); this._warnMentionedGroups(preview); this._warnCannotSeeMention(preview); - this._warnHereMention(r.here_count); + this._warnHereMention(response.here_count); }); }, @@ -506,28 +508,27 @@ export default Component.extend(ComposerUploadUppy, { } }, + @debounce(2000) _warnMentionedGroups(preview) { schedule("afterRender", () => { - let found = this.warnedGroupMentions || []; - preview?.querySelectorAll(".mention-group.notify")?.forEach((mention) => { - if (this._isInQuote(mention)) { - return; - } + preview + .querySelectorAll(".mention-group[data-mentionable-user-count]") + .forEach((mention) => { + const { name } = mention.dataset; + if ( + this.warnedGroupMentions.includes(name) || + this._isInQuote(mention) + ) { + return; + } - let name = mention.dataset.name; - if (!found.includes(name)) { - this.groupsMentioned([ - { - name, - user_count: mention.dataset.mentionableUserCount, - max_mentions: mention.dataset.maxMentions, - }, - ]); - found.push(name); - } - }); - - this.set("warnedGroupMentions", found); + this.warnedGroupMentions.push(name); + this.groupsMentioned({ + name, + userCount: mention.dataset.mentionableUserCount, + maxMentions: mention.dataset.maxMentions, + }); + }); }); }, @@ -539,22 +540,35 @@ export default Component.extend(ComposerUploadUppy, { return; } - const warnings = []; - - preview.querySelectorAll(".mention.cannot-see").forEach((mention) => { + preview.querySelectorAll(".mention[data-reason]").forEach((mention) => { const { name } = mention.dataset; - if (this.warnedCannotSeeMentions.includes(name)) { return; } this.warnedCannotSeeMentions.push(name); - warnings.push({ name, reason: cannotSee[name] }); + this.cannotSeeMention({ + name, + reason: mention.dataset.reason, + }); }); - if (warnings.length > 0) { - this.cannotSeeMention(warnings); - } + preview + .querySelectorAll(".mention-group[data-reason]") + .forEach((mention) => { + const { name } = mention.dataset; + if (this.warnedCannotSeeMentions.includes(name)) { + return; + } + + this.warnedCannotSeeMentions.push(name); + this.cannotSeeMention({ + name, + reason: mention.dataset.reason, + notifiedCount: mention.dataset.notifiedUserCount, + isGroup: true, + }); + }); }, _warnHereMention(hereCount) { @@ -562,13 +576,7 @@ export default Component.extend(ComposerUploadUppy, { return; } - discourseLater( - this, - () => { - this.hereMention(hereCount); - }, - 2000 - ); + this.hereMention(hereCount); }, @bind diff --git a/app/assets/javascripts/discourse/app/components/d-editor.js b/app/assets/javascripts/discourse/app/components/d-editor.js index 64175e030f..a622955b4c 100644 --- a/app/assets/javascripts/discourse/app/components/d-editor.js +++ b/app/assets/javascripts/discourse/app/components/d-editor.js @@ -464,9 +464,11 @@ export default Component.extend(TextareaTextManipulation, { this.site.hashtag_configurations["topic-composer"], this._$textarea, this.siteSettings, - (value) => { - this.set("value", value); - schedule("afterRender", this, this.focusTextArea); + { + afterComplete: (value) => { + this.set("value", value); + schedule("afterRender", this, this.focusTextArea); + }, } ); }, diff --git a/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js b/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js index f499f6e3f5..456787d5ba 100644 --- a/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js +++ b/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js @@ -84,7 +84,7 @@ export default Component.extend({ @discourseComputed() timeOptions() { - const timezone = this.currentUser.timezone; + const timezone = this.currentUser.user_option.timezone; const shortcuts = timeShortcuts(timezone); return [ diff --git a/app/assets/javascripts/discourse/app/components/future-date-input.js b/app/assets/javascripts/discourse/app/components/future-date-input.js index 70c4a98831..0c3257410f 100644 --- a/app/assets/javascripts/discourse/app/components/future-date-input.js +++ b/app/assets/javascripts/discourse/app/components/future-date-input.js @@ -28,7 +28,7 @@ export default Component.extend({ init() { this._super(...arguments); - this.userTimezone = this.currentUser.timezone; + this.userTimezone = this.currentUser.user_option.timezone; }, didReceiveAttrs() { diff --git a/app/assets/javascripts/discourse/app/components/glimmer-topic-timeline.hbs b/app/assets/javascripts/discourse/app/components/glimmer-topic-timeline.hbs index d7243af8a5..6f557278e3 100644 --- a/app/assets/javascripts/discourse/app/components/glimmer-topic-timeline.hbs +++ b/app/assets/javascripts/discourse/app/components/glimmer-topic-timeline.hbs @@ -7,6 +7,7 @@ @jumpBottom={{@jumpBottom}} @jumpEnd={{@jumpEnd}} @jumpToIndex={{@jumpToIndex}} + @jumpToPostPrompt={{@jumpToPostPrompt}} @fullscreen={{@fullscreen}} @mobileView={{@mobileView}} @currentUser={{this.currentUser}} @@ -23,50 +24,7 @@ @resetBumpDate={{@resetBumpDate}} @convertToPublicTopic={{@convertToPublicTopic}} @convertToPrivateMessage={{@convertToPrivateMessage}} + @replyToPost={{@replyToPost}} /> - -
    diff --git a/app/assets/javascripts/discourse/app/components/glimmer-topic-timeline.js b/app/assets/javascripts/discourse/app/components/glimmer-topic-timeline.js index 814fb951cc..7c7f788932 100644 --- a/app/assets/javascripts/discourse/app/components/glimmer-topic-timeline.js +++ b/app/assets/javascripts/discourse/app/components/glimmer-topic-timeline.js @@ -79,10 +79,6 @@ export default class GlimmerTopicTimeline extends Component { return this.args.fullscreen && !this.args.addShowClass; } - get canCreatePost() { - return this.args.model.details?.can_create_post; - } - get createdAt() { return new Date(this.args.model.created_at); } diff --git a/app/assets/javascripts/discourse/app/components/quote-button.js b/app/assets/javascripts/discourse/app/components/quote-button.js index a5066f754a..3c057ce72f 100644 --- a/app/assets/javascripts/discourse/app/components/quote-button.js +++ b/app/assets/javascripts/discourse/app/components/quote-button.js @@ -424,7 +424,7 @@ export default Component.extend(KeyEnterEscape, { embedQuoteButton(canCreatePost, canReplyAsNewTopic) { return ( (canCreatePost || canReplyAsNewTopic) && - this.currentUser?.get("enable_quoting") + this.currentUser?.get("user_option.enable_quoting") ); }, diff --git a/app/assets/javascripts/discourse/app/components/second-factor-form.js b/app/assets/javascripts/discourse/app/components/second-factor-form.js index 9b23ed64ac..e208a5f314 100644 --- a/app/assets/javascripts/discourse/app/components/second-factor-form.js +++ b/app/assets/javascripts/discourse/app/components/second-factor-form.js @@ -42,12 +42,10 @@ export default Component.extend({ } }, - @discourseComputed("backupEnabled", "totpEnabled", "secondFactorMethod") - showToggleMethodLink(backupEnabled, totpEnabled, secondFactorMethod) { + @discourseComputed("backupEnabled", "secondFactorMethod") + showToggleMethodLink(backupEnabled, secondFactorMethod) { return ( - backupEnabled && - totpEnabled && - secondFactorMethod !== SECOND_FACTOR_METHODS.SECURITY_KEY + backupEnabled && secondFactorMethod !== SECOND_FACTOR_METHODS.SECURITY_KEY ); }, diff --git a/app/assets/javascripts/discourse/app/components/site-header.js b/app/assets/javascripts/discourse/app/components/site-header.js index 73b82710dd..b8c146d932 100644 --- a/app/assets/javascripts/discourse/app/components/site-header.js +++ b/app/assets/javascripts/discourse/app/components/site-header.js @@ -42,10 +42,9 @@ const SiteHeaderComponent = MountWidget.extend( @observes("site.narrowDesktopView") narrowDesktopViewChanged() { - if ( - this.siteSettings.enable_experimental_sidebar_hamburger && - (!this.sidebarEnabled || this.site.narrowDesktopView) - ) { + this.eventDispatched("dom:clean", "header"); + + if (this._dropDownHeaderEnabled()) { this.appEvents.on( "sidebar-hamburger-dropdown:rendered", this, @@ -231,10 +230,7 @@ const SiteHeaderComponent = MountWidget.extend( this.appEvents.on("user-menu:rendered", this, "_animateMenu"); } - if ( - this.siteSettings.enable_experimental_sidebar_hamburger && - (!this.sidebarEnabled || this.site.narrowDesktopView) - ) { + if (this._dropDownHeaderEnabled()) { this.appEvents.on( "sidebar-hamburger-dropdown:rendered", this, @@ -323,10 +319,7 @@ const SiteHeaderComponent = MountWidget.extend( this.appEvents.off("user-menu:rendered", this, "_animateMenu"); } - if ( - this.siteSettings.enable_experimental_sidebar_hamburger && - !this.sidebarEnabled - ) { + if (this._dropDownHeaderEnabled()) { this.appEvents.off( "sidebar-hamburger-dropdown:rendered", this, @@ -468,6 +461,14 @@ const SiteHeaderComponent = MountWidget.extend( this._animate = false; }); }, + + _dropDownHeaderEnabled() { + return ( + (!this.sidebarEnabled && + this.siteSettings.navigation_menu !== "legacy") || + this.site.narrowDesktopView + ); + }, } ); diff --git a/app/assets/javascripts/discourse/app/components/software-update-prompt.js b/app/assets/javascripts/discourse/app/components/software-update-prompt.js index 6d32dd77e5..21abaa87bc 100644 --- a/app/assets/javascripts/discourse/app/components/software-update-prompt.js +++ b/app/assets/javascripts/discourse/app/components/software-update-prompt.js @@ -1,7 +1,7 @@ import getURL from "discourse-common/lib/get-url"; import { cancel } from "@ember/runloop"; import discourseLater from "discourse-common/lib/later"; -import discourseComputed, { on } from "discourse-common/utils/decorators"; +import discourseComputed, { bind, on } from "discourse-common/utils/decorators"; import Component from "@ember/component"; import { action } from "@ember/object"; import { isTesting } from "discourse-common/config/environment"; @@ -13,36 +13,49 @@ export default Component.extend({ animatePrompt: false, _timeoutHandler: null, + init() { + this._super(...arguments); + + this.messageBus.subscribe("/refresh_client", this.onRefresh); + this.messageBus.subscribe("/global/asset-version", this.onAsset); + }, + + willDestroy() { + this._super(...arguments); + + this.messageBus.unsubscribe("/refresh_client", this.onRefresh); + this.messageBus.unsubscribe("/global/asset-version", this.onAsset); + }, + + @bind + onRefresh() { + this.session.requiresRefresh = true; + }, + + @bind + onAsset(version) { + if (this.session.assetVersion !== version) { + this.session.requiresRefresh = true; + } + + if (!this._timeoutHandler && this.session.requiresRefresh) { + if (isTesting()) { + this.updatePromptState(true); + } else { + // Since we can do this transparently for people browsing the forum + // hold back the message 24 hours. + this._timeoutHandler = discourseLater(() => { + this.updatePromptState(true); + }, 1000 * 60 * 24 * 60); + } + } + }, + @discourseComputed rootUrl() { return getURL("/"); }, - @on("init") - initSubscriptions() { - this.messageBus.subscribe("/refresh_client", () => { - this.session.requiresRefresh = true; - }); - - this.messageBus.subscribe("/global/asset-version", (version) => { - if (this.session.assetVersion !== version) { - this.session.requiresRefresh = true; - } - - if (!this._timeoutHandler && this.session.requiresRefresh) { - if (isTesting()) { - this.updatePromptState(true); - } else { - // Since we can do this transparently for people browsing the forum - // hold back the message 24 hours. - this._timeoutHandler = discourseLater(() => { - this.updatePromptState(true); - }, 1000 * 60 * 24 * 60); - } - } - }); - }, - updatePromptState(value) { // when adding the message, we inject the HTML then add the animation // when dismissing, things need to happen in the opposite order diff --git a/app/assets/javascripts/discourse/app/components/suggested-topics.js b/app/assets/javascripts/discourse/app/components/suggested-topics.js index a054bb0a76..ad0c667170 100644 --- a/app/assets/javascripts/discourse/app/components/suggested-topics.js +++ b/app/assets/javascripts/discourse/app/components/suggested-topics.js @@ -1,7 +1,6 @@ import { computed, get } from "@ember/object"; import Component from "@ember/component"; import I18n from "I18n"; -import Site from "discourse/models/site"; import { categoryBadgeHTML } from "discourse/helpers/category-link"; import discourseComputed from "discourse-common/utils/decorators"; import getURL from "discourse-common/lib/get-url"; @@ -51,7 +50,7 @@ export default Component.extend({ if (suggestedGroupName) { return I18n.messageFormat("user.messages.read_more_group_pm_MF", { - BOTH: hasBoth, + HAS_UNREAD_AND_NEW: hasBoth, UNREAD: unreadCount, NEW: newCount, username, @@ -61,7 +60,7 @@ export default Component.extend({ }); } else { return I18n.messageFormat("user.messages.read_more_personal_pm_MF", { - BOTH: hasBoth, + HAS_UNREAD_AND_NEW: hasBoth, UNREAD: unreadCount, NEW: newCount, username, @@ -81,31 +80,15 @@ export default Component.extend({ }, _topicBrowseMoreMessage(topic) { - const opts = { - latestLink: `${I18n.t( - "topic.view_latest_topics" - )}`, - }; let category = topic.get("category"); if ( category && - get(category, "id") === Site.currentProp("uncategorized_category_id") + get(category, "id") === this.site.uncategorized_category_id ) { category = null; } - if (category) { - opts.catLink = categoryBadgeHTML(category); - } else { - opts.catLink = - '' + - I18n.t("topic.browse_all_categories") + - ""; - } - let unreadTopics = 0; let newTopics = 0; @@ -115,21 +98,24 @@ export default Component.extend({ } if (newTopics + unreadTopics > 0) { - const hasBoth = unreadTopics > 0 && newTopics > 0; - return I18n.messageFormat("topic.read_more_MF", { - BOTH: hasBoth, + HAS_UNREAD_AND_NEW: unreadTopics > 0 && newTopics > 0, UNREAD: unreadTopics, NEW: newTopics, - CATEGORY: category ? true : false, - latestLink: opts.latestLink, - catLink: opts.catLink, + HAS_CATEGORY: category ? true : false, + categoryLink: category ? categoryBadgeHTML(category) : null, basePath: getURL(""), }); } else if (category) { - return I18n.t("topic.read_more_in_category", opts); + return I18n.t("topic.read_more_in_category", { + categoryLink: categoryBadgeHTML(category), + latestLink: getURL("/latest"), + }); } else { - return I18n.t("topic.read_more", opts); + return I18n.t("topic.read_more", { + categoryLink: getURL("/categories"), + latestLink: getURL("/latest"), + }); } }, diff --git a/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js b/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js index 26a65a0f1c..504a6043fd 100644 --- a/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js +++ b/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js @@ -68,7 +68,7 @@ export default Component.extend({ @on("init") _setupPicker() { this.setProperties({ - userTimezone: this.currentUser.timezone, + userTimezone: this.currentUser.user_option.timezone, hiddenOptions: this.hiddenOptions || [], customOptions: this.customOptions || [], customLabels: this.customLabels || {}, diff --git a/app/assets/javascripts/discourse/app/components/topic-footer-buttons.js b/app/assets/javascripts/discourse/app/components/topic-footer-buttons.js index 5f91ecca8e..af67f35d26 100644 --- a/app/assets/javascripts/discourse/app/components/topic-footer-buttons.js +++ b/app/assets/javascripts/discourse/app/components/topic-footer-buttons.js @@ -56,7 +56,7 @@ export default Component.extend({ canInviteTo: alias("topic.details.can_invite_to"), - canDefer: alias("currentUser.enable_defer"), + canDefer: alias("currentUser.user_option.enable_defer"), inviteDisabled: or("topic.archived", "topic.closed", "topic.deleted"), diff --git a/app/assets/javascripts/discourse/app/components/topic-list-item.js b/app/assets/javascripts/discourse/app/components/topic-list-item.js index 2d6ce3e18f..16247e89c8 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list-item.js +++ b/app/assets/javascripts/discourse/app/components/topic-list-item.js @@ -77,13 +77,7 @@ export default Component.extend({ this._super(...arguments); if (this.includeUnreadIndicator) { - this.messageBus.subscribe(this.unreadIndicatorChannel, (data) => { - const nodeClassList = document.querySelector( - `.indicator-topic-${data.topic_id}` - ).classList; - - nodeClassList.toggle("read", !data.show_indicator); - }); + this.messageBus.subscribe(this.unreadIndicatorChannel, this.onMessage); } schedule("afterRender", () => { @@ -101,9 +95,8 @@ export default Component.extend({ willDestroyElement() { this._super(...arguments); - if (this.includeUnreadIndicator) { - this.messageBus.unsubscribe(this.unreadIndicatorChannel); - } + this.messageBus.unsubscribe(this.unreadIndicatorChannel, this.onMessage); + if (this._shouldFocusLastVisited()) { const title = this._titleElement(); if (title) { @@ -113,6 +106,15 @@ export default Component.extend({ } }, + @bind + onMessage(data) { + const nodeClassList = document.querySelector( + `.indicator-topic-${data.topic_id}` + ).classList; + + nodeClassList.toggle("read", !data.show_indicator); + }, + @discourseComputed("topic.id") unreadIndicatorChannel(topicId) { return `/private-messages/unread-indicator/${topicId}`; diff --git a/app/assets/javascripts/discourse/app/components/topic-status.js b/app/assets/javascripts/discourse/app/components/topic-status.js index 9b67687ebe..85651c692d 100644 --- a/app/assets/javascripts/discourse/app/components/topic-status.js +++ b/app/assets/javascripts/discourse/app/components/topic-status.js @@ -79,7 +79,7 @@ export default Component.extend({ : this._reset("invisible"); }, - _set(name, icon, key, iconArgs = null) { + _set(name, icon, key, iconArgs) { this.set(`${name}Icon`, htmlSafe(iconHTML(`${icon}`, iconArgs))); this.set(`${name}Title`, I18n.t(`topic_statuses.${key}.help`)); return true; diff --git a/app/assets/javascripts/discourse/app/components/topic-timeline/container.hbs b/app/assets/javascripts/discourse/app/components/topic-timeline/container.hbs index 93fce8a091..0148696dc9 100644 --- a/app/assets/javascripts/discourse/app/components/topic-timeline/container.hbs +++ b/app/assets/javascripts/discourse/app/components/topic-timeline/container.hbs @@ -1,7 +1,7 @@ {{#if @fullscreen}}

    - {{if @mobileView @model.fancyTitle ""}} + {{this.topicTitle}}

    {{#if (or this.siteSettings.topic_featured_link_enabled this.showTags)}}
    @@ -94,4 +94,48 @@
    + + {{/if}} diff --git a/app/assets/javascripts/discourse/app/components/topic-timeline/container.js b/app/assets/javascripts/discourse/app/components/topic-timeline/container.js index 9442219d48..9ed5d571b9 100644 --- a/app/assets/javascripts/discourse/app/components/topic-timeline/container.js +++ b/app/assets/javascripts/discourse/app/components/topic-timeline/container.js @@ -27,7 +27,6 @@ export default class TopicTimelineScrollArea extends Component { @tracked total; @tracked date; @tracked lastReadPercentage = null; - @tracked displayTimeLineScrollArea = true; @tracked before; @tracked after; @tracked timelineScrollareaStyle; @@ -38,15 +37,6 @@ export default class TopicTimelineScrollArea extends Component { super(...arguments); if (!this.args.mobileView) { - const streamLength = this.args.model.postStream?.stream?.length; - - if (streamLength === 1) { - const postsWrapper = document.querySelector(".posts-wrapper"); - if (postsWrapper && postsWrapper.offsetHeight < 1000) { - this.displayTimeLineScrollArea = false; - } - } - // listen for scrolling event to update timeline this.appEvents.on("topic:current-post-scrolled", this.postScrolled); // listen for composer sizing changes to update timeline @@ -58,6 +48,30 @@ export default class TopicTimelineScrollArea extends Component { this.calculatePosition(); } + get displayTimeLineScrollArea() { + if (this.args.mobileView) { + return true; + } + + const streamLength = this.args.model.postStream?.stream?.length; + if (streamLength === 1) { + const postsWrapper = document.querySelector(".posts-wrapper"); + if (postsWrapper && postsWrapper.offsetHeight < 1000) { + return false; + } + } + + return true; + } + + get canCreatePost() { + return this.args.model.details?.can_create_post; + } + + get topicTitle() { + return htmlSafe(this.args.mobileView ? this.args.model.fancyTitle : ""); + } + get showTags() { return ( this.siteSettings.tagging_enabled && this.args.model.tags?.length > 0 diff --git a/app/assets/javascripts/discourse/app/components/user-card-contents.js b/app/assets/javascripts/discourse/app/components/user-card-contents.js index 34e3147745..884fcf35d3 100644 --- a/app/assets/javascripts/discourse/app/components/user-card-contents.js +++ b/app/assets/javascripts/discourse/app/components/user-card-contents.js @@ -92,7 +92,7 @@ export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, { if (!this.showUserLocalTime) { return; } - return user.timezone; + return user.get("user_option.timezone"); }, @discourseComputed("userTimezone") diff --git a/app/assets/javascripts/discourse/app/components/user-menu/menu.js b/app/assets/javascripts/discourse/app/components/user-menu/menu.js index 89ff33435e..fc963747a3 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/menu.js +++ b/app/assets/javascripts/discourse/app/components/user-menu/menu.js @@ -83,7 +83,7 @@ const CORE_TOP_TABS = [ } get shouldDisplay() { - return !this.currentUser.likes_notifications_disabled; + return !this.currentUser.user_option.likes_notifications_disabled; } get count() { diff --git a/app/assets/javascripts/discourse/app/components/user-nav/preferences-nav.hbs b/app/assets/javascripts/discourse/app/components/user-nav/preferences-nav.hbs index 45a007b6c3..c097edb7b7 100644 --- a/app/assets/javascripts/discourse/app/components/user-nav/preferences-nav.hbs +++ b/app/assets/javascripts/discourse/app/components/user-nav/preferences-nav.hbs @@ -56,7 +56,7 @@ -{{#if @siteSettings.enable_experimental_sidebar_hamburger}} +{{#if (not (eq @siteSettings.navigation_menu "legacy"))}}
    - + {{#if this.showSecurityKey}} diff --git a/app/assets/javascripts/discourse/app/templates/modal/second-factor-backup-edit.hbs b/app/assets/javascripts/discourse/app/templates/modal/second-factor-backup-edit.hbs index 0316c260f5..e9cbe827a6 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/second-factor-backup-edit.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/second-factor-backup-edit.hbs @@ -20,7 +20,6 @@
    {{#if this.backupEnabled}} - {{else}} {{/if}} diff --git a/app/assets/javascripts/discourse/app/templates/modal/second-factor-edit.hbs b/app/assets/javascripts/discourse/app/templates/modal/second-factor-edit.hbs index 63d3980b1b..e544da78d4 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/second-factor-edit.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/second-factor-edit.hbs @@ -9,5 +9,4 @@ diff --git a/app/assets/javascripts/discourse/app/templates/preferences-second-factor.hbs b/app/assets/javascripts/discourse/app/templates/preferences-second-factor.hbs index 166a2c81d9..4e17f0c92a 100644 --- a/app/assets/javascripts/discourse/app/templates/preferences-second-factor.hbs +++ b/app/assets/javascripts/discourse/app/templates/preferences-second-factor.hbs @@ -30,59 +30,79 @@

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

    - {{#each this.totps as |totp|}} -
    - {{#if totp.name}} - {{totp.name}} - {{else}} - {{i18n "user.second_factor.totp.default_name"}} - {{/if}} +
    +
    + {{#if totp.name}} + {{totp.name}} + {{else}} + {{i18n "user.second_factor.totp.default_name"}} + {{/if}} +
    {{#if this.isCurrentUser}} - +
    + + +
    {{/if}}
    {{/each}} +

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

    - {{#each this.security_keys as |security_key|}} -
    - {{#if security_key.name}} - {{security_key.name}} - {{else}} - {{i18n "user.second_factor.security_key.default_name"}} - {{/if}} +
    +
    + {{#if security_key.name}} + {{security_key.name}} + {{else}} + {{i18n "user.second_factor.security_key.default_name"}} + {{/if}} +
    {{#if this.isCurrentUser}} - +
    + + +
    {{/if}}
    {{/each}} +

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

    - {{#if this.model.second_factor_enabled}} - {{#if this.model.second_factor_backup_enabled}} - {{html-safe (i18n "user.second_factor_backup.manage" count=this.model.second_factor_remaining_backup_codes)}} - {{else}} - {{i18n "user.second_factor_backup.enable_long"}} - {{/if}} +
    + {{#if this.model.second_factor_enabled}} +
    + {{#if this.model.second_factor_backup_enabled}} + {{html-safe (i18n "user.second_factor_backup.manage" count=this.model.second_factor_remaining_backup_codes)}} + {{else}} + {{i18n "user.second_factor_backup.enable_long"}} + {{/if}} +
    - {{#if this.isCurrentUser}} - + {{#if this.isCurrentUser}} +
    + + + {{#if this.model.second_factor_backup_enabled}} + + {{/if}} +
    + {{/if}} + {{else}} + {{i18n "user.second_factor_backup.enable_prerequisites"}} {{/if}} - {{else}} - {{i18n "user.second_factor_backup.enable_prerequisites"}} - {{/if}} +
    diff --git a/app/assets/javascripts/discourse/app/templates/preferences-username.hbs b/app/assets/javascripts/discourse/app/templates/preferences-username.hbs deleted file mode 100644 index 9d4e1ec997..0000000000 --- a/app/assets/javascripts/discourse/app/templates/preferences-username.hbs +++ /dev/null @@ -1,38 +0,0 @@ - -
    -
    - -
    -
    -

    {{i18n "user.change_username.title"}}

    -
    -
    - -
    - -
    - -
    -
    -

    - {{#if this.taken}} - {{i18n "user.change_username.taken"}} - {{/if}} - {{this.errorMessage}} -

    -
    -
    - -
    -
    - - - {{i18n "cancel"}} - - {{#if this.saved}}{{i18n "saved"}}{{/if}} -
    -
    - -
    -
    -
    diff --git a/app/assets/javascripts/discourse/app/templates/preferences.hbs b/app/assets/javascripts/discourse/app/templates/preferences.hbs index 21758e3d4f..c39afcbb8b 100644 --- a/app/assets/javascripts/discourse/app/templates/preferences.hbs +++ b/app/assets/javascripts/discourse/app/templates/preferences.hbs @@ -61,7 +61,7 @@ - {{#if this.siteSettings.enable_experimental_sidebar_hamburger}} + {{#if (not (eq @siteSettings.navigation_menu "legacy"))}} - - {{outlet}} diff --git a/app/assets/javascripts/discourse/app/templates/user/messages.hbs b/app/assets/javascripts/discourse/app/templates/user/messages.hbs index cf4c71d1d0..a7b8988e09 100644 --- a/app/assets/javascripts/discourse/app/templates/user/messages.hbs +++ b/app/assets/javascripts/discourse/app/templates/user/messages.hbs @@ -13,17 +13,21 @@ - {{#if this.site.desktopView}} -
    {{else}} @@ -127,16 +131,17 @@ {{/if}}
    -
    - {{#if this.site.mobileView}} - {{#if this.showNewPM}} - + {{#unless this.currentUser.redesigned_user_page_nav_enabled}} +
    + {{#if this.site.mobileView}} + {{#if this.showNewPM}} + + {{/if}} + {{#if this.currentUser.admin}} + + {{/if}} {{/if}} - {{#if this.currentUser.admin}} - - {{/if}} - {{/if}} -
    - +
    + {{/unless}} {{outlet}}
    diff --git a/app/assets/javascripts/discourse/app/widgets/header-contents.js b/app/assets/javascripts/discourse/app/widgets/header-contents.js index 7a5ccf8df4..ba579498da 100644 --- a/app/assets/javascripts/discourse/app/widgets/header-contents.js +++ b/app/assets/javascripts/discourse/app/widgets/header-contents.js @@ -5,16 +5,17 @@ createWidget("header-contents", { tagName: "div.contents.clearfix", template: hbs` {{#if this.site.desktopView}} - {{#if this.siteSettings.enable_experimental_sidebar_hamburger}} - {{#if attrs.sidebarEnabled}} - {{sidebar-toggle attrs=attrs}} - {{/if}} + {{#if attrs.sidebarEnabled}} + {{sidebar-toggle attrs=attrs}} {{/if}} {{/if}} + {{home-logo attrs=attrs}} + {{#if attrs.topic}} {{header-topic-info attrs=attrs}} {{/if}} + `, }); diff --git a/app/assets/javascripts/discourse/app/widgets/header.js b/app/assets/javascripts/discourse/app/widgets/header.js index 81f705a2e6..01c1910112 100644 --- a/app/assets/javascripts/discourse/app/widgets/header.js +++ b/app/assets/javascripts/discourse/app/widgets/header.js @@ -324,9 +324,8 @@ createWidget("header-icons", { }); if ( - !this.siteSettings.enable_experimental_sidebar_hamburger || - (this.siteSettings.enable_experimental_sidebar_hamburger && - !attrs.sidebarEnabled) || + this.siteSettings.navigation_menu === "legacy" || + !attrs.sidebarEnabled || this.site.mobileView ) { icons.push(hamburger); @@ -498,7 +497,7 @@ export default createWidget("header", { }) ); } else if (state.hamburgerVisible) { - if (this.siteSettings.enable_experimental_sidebar_hamburger) { + if (this.siteSettings.navigation_menu !== "legacy") { if (!attrs.sidebarEnabled || this.site.narrowDesktopView) { panels.push(this.attach("revamped-hamburger-menu-wrapper", {})); } @@ -613,7 +612,7 @@ export default createWidget("header", { toggleHamburger() { if ( - this.siteSettings.enable_experimental_sidebar_hamburger && + this.siteSettings.navigation_menu !== "legacy" && this.attrs.sidebarEnabled && !this.site.narrowDesktopView ) { @@ -623,7 +622,7 @@ export default createWidget("header", { this.toggleBodyScrolling(this.state.hamburgerVisible); schedule("afterRender", () => { - if (this.siteSettings.enable_experimental_sidebar_hamburger) { + if (this.siteSettings.navigation_menu !== "legacy") { // Remove focus from hamburger toggle button document.querySelector("#toggle-hamburger-menu")?.blur(); } else { diff --git a/app/assets/javascripts/discourse/app/widgets/post-cooked.js b/app/assets/javascripts/discourse/app/widgets/post-cooked.js index 62941eaf60..2fd95732d9 100644 --- a/app/assets/javascripts/discourse/app/widgets/post-cooked.js +++ b/app/assets/javascripts/discourse/app/widgets/post-cooked.js @@ -4,10 +4,13 @@ import { ajax } from "discourse/lib/ajax"; import highlightSearch from "discourse/lib/highlight-search"; import { iconHTML } from "discourse-common/lib/icon-library"; import { isValidLink } from "discourse/lib/click-track"; -import { number } from "discourse/lib/formatter"; +import { number, until } from "discourse/lib/formatter"; import { spinnerHTML } from "discourse/helpers/loading-spinner"; import { escape } from "pretty-text/sanitizer"; import domFromString from "discourse-common/lib/dom-from-string"; +import getURL from "discourse-common/lib/get-url"; +import { emojiUnescape } from "discourse/lib/text"; +import { escapeExpression } from "discourse/lib/utilities"; let _beforeAdoptDecorators = []; let _afterAdoptDecorators = []; @@ -57,16 +60,25 @@ export default class PostCooked { init() { this.originalQuoteContents = null; + // todo should be a better way of detecting if it is composer preview + this._isInComposerPreview = !this.decoratorHelper; + const cookedDiv = this._computeCooked(); + this.cookedDiv = cookedDiv; this._insertQuoteControls(cookedDiv); this._showLinkCounts(cookedDiv); this._applySearchHighlight(cookedDiv); + this._initUserStatusOnMentions(); this._decorateAndAdopt(cookedDiv); return cookedDiv; } + destroy() { + this._stopTrackingMentionedUsersStatus(); + } + _decorateAndAdopt(cooked) { _beforeAdoptDecorators.forEach((d) => d(cooked, this.decoratorHelper)); @@ -200,7 +212,7 @@ export default class PostCooked { try { const result = await ajax(`/posts/by_number/${topicId}/${postId}`); - const post = this.decoratorHelper.getModel(); + const post = this._post(); const quotedPosts = post.quoted || {}; quotedPosts[result.id] = result; post.set("quoted", quotedPosts); @@ -361,6 +373,79 @@ export default class PostCooked { return cookedDiv; } + + _initUserStatusOnMentions() { + if (!this._isInComposerPreview) { + this._trackMentionedUsersStatus(); + this._rerenderUserStatusOnMentions(); + } + } + + _rerenderUserStatusOnMentions() { + this._post()?.mentioned_users?.forEach((user) => + this._rerenderUserStatusOnMention(this.cookedDiv, user) + ); + } + + _rerenderUserStatusOnMention(postElement, user) { + const href = getURL(`/u/${user.username.toLowerCase()}`); + const mentions = postElement.querySelectorAll(`a.mention[href="${href}"]`); + + mentions.forEach((mention) => { + this._updateUserStatus(mention, user.status); + }); + } + + _updateUserStatus(mention, status) { + this._removeUserStatus(mention); + if (status) { + this._insertUserStatus(mention, status); + } + } + + _insertUserStatus(mention, status) { + const emoji = escapeExpression(`:${status.emoji}:`); + const statusHtml = emojiUnescape(emoji, { + class: "user-status", + title: this._userStatusTitle(status), + }); + mention.insertAdjacentHTML("beforeend", statusHtml); + } + + _removeUserStatus(mention) { + mention.querySelector("img.user-status")?.remove(); + } + + _userStatusTitle(status) { + if (!status.ends_at) { + return status.description; + } + + const until_ = until( + status.ends_at, + this.currentUser.timezone, + this.currentUser.locale + ); + return escapeExpression(`${status.description} ${until_}`); + } + + _trackMentionedUsersStatus() { + this._post()?.mentioned_users?.forEach((user) => { + user.trackStatus(); + user.on("status-changed", this, "_rerenderUserStatusOnMentions"); + }); + } + + _stopTrackingMentionedUsersStatus() { + this._post()?.mentioned_users?.forEach((user) => { + user.stopTrackingStatus(); + user.off("status-changed", this, "_rerenderUserStatusOnMentions"); + }); + } + + _post() { + return this.decoratorHelper?.getModel?.(); + } } PostCooked.prototype.type = "Widget"; diff --git a/app/assets/javascripts/discourse/app/widgets/post-menu.js b/app/assets/javascripts/discourse/app/widgets/post-menu.js index 3b0e10f8b3..4eaa333b7c 100644 --- a/app/assets/javascripts/discourse/app/widgets/post-menu.js +++ b/app/assets/javascripts/discourse/app/widgets/post-menu.js @@ -353,7 +353,7 @@ registerButton( if (attrs.bookmarkReminderAt) { let formattedReminder = formattedReminderTime( attrs.bookmarkReminderAt, - currentUser.timezone + currentUser.user_option.timezone ); title = "bookmarks.created_with_reminder"; titleOptions.date = formattedReminder; diff --git a/app/assets/javascripts/discourse/app/widgets/post-small-action.js b/app/assets/javascripts/discourse/app/widgets/post-small-action.js index e13857f331..246de3c030 100644 --- a/app/assets/javascripts/discourse/app/widgets/post-small-action.js +++ b/app/assets/javascripts/discourse/app/widgets/post-small-action.js @@ -97,39 +97,8 @@ export default createWidget("post-small-action", { html(attrs) { const contents = []; - - if (attrs.canRecover) { - contents.push( - this.attach("button", { - className: "small-action-recover", - icon: "undo", - action: "recoverPost", - title: "post.controls.undelete", - }) - ); - } - - if (attrs.canDelete) { - contents.push( - this.attach("button", { - className: "small-action-delete", - icon: "trash-alt", - action: "deletePost", - title: "post.controls.delete", - }) - ); - } - - if (attrs.canEdit && !attrs.canRecover) { - contents.push( - this.attach("button", { - className: "small-action-edit", - icon: "pencil-alt", - action: "editPost", - title: "post.controls.edit", - }) - ); - } + const buttons = []; + const customMessage = []; contents.push( avatarFor.call(this, "small", { @@ -149,19 +118,56 @@ export default createWidget("post-small-action", { attrs.actionCodePath ); contents.push(new RawHtml({ html: `

    ${description}

    ` })); + } - if (attrs.cooked) { - contents.push( - new RawHtml({ - html: `
    ${attrs.cooked}
    `, - }) - ); - } + if (attrs.canRecover) { + buttons.push( + this.attach("button", { + className: "btn-flat small-action-recover", + icon: "undo", + action: "recoverPost", + title: "post.controls.undelete", + }) + ); + } + + if (attrs.canEdit && !attrs.canRecover) { + buttons.push( + this.attach("button", { + className: "btn-flat small-action-edit", + icon: "pencil-alt", + action: "editPost", + title: "post.controls.edit", + }) + ); + } + + if (attrs.canDelete) { + buttons.push( + this.attach("button", { + className: "btn-flat btn-danger small-action-delete", + icon: "trash-alt", + action: "deletePost", + title: "post.controls.delete", + }) + ); + } + + if (!attrs.actionDescriptionWidget && attrs.cooked) { + customMessage.push( + new RawHtml({ + html: `
    ${attrs.cooked}
    `, + }) + ); } return [ h("div.topic-avatar", iconNode(icons[attrs.actionCode] || "exclamation")), - h("div.small-action-desc", contents), + h("div.small-action-desc", [ + h("div.small-action-contents", contents), + h("div.small-action-buttons", buttons), + customMessage, + ]), ]; }, }); diff --git a/app/assets/javascripts/discourse/app/widgets/search-menu-results.js b/app/assets/javascripts/discourse/app/widgets/search-menu-results.js index ab636a93ff..bc88e254e5 100644 --- a/app/assets/javascripts/discourse/app/widgets/search-menu-results.js +++ b/app/assets/javascripts/discourse/app/widgets/search-menu-results.js @@ -364,7 +364,7 @@ createWidget("search-menu-results", { if (["topic"].includes(rt.type)) { const more = buildMoreNode(rt); if (more) { - resultNodeContents.push(h("div.show-more", more)); + resultNodeContents.push(h("div.search-menu__show-more", more)); } } diff --git a/app/assets/javascripts/discourse/app/widgets/topic-status.js b/app/assets/javascripts/discourse/app/widgets/topic-status.js index 11d3e35030..39c6de6340 100644 --- a/app/assets/javascripts/discourse/app/widgets/topic-status.js +++ b/app/assets/javascripts/discourse/app/widgets/topic-status.js @@ -15,7 +15,7 @@ export default createWidget("topic-status", { const result = []; TopicStatusIcons.render(topic, function (name, key) { - const iconArgs = key === "unpinned" ? { class: "unpinned" } : null; + const iconArgs = { class: key === "unpinned" ? "unpinned" : null }; const icon = iconNode(name, iconArgs); const attributes = { diff --git a/app/assets/javascripts/discourse/app/widgets/user-status-bubble.js b/app/assets/javascripts/discourse/app/widgets/user-status-bubble.js index ec814670ad..01dd33b7bf 100644 --- a/app/assets/javascripts/discourse/app/widgets/user-status-bubble.js +++ b/app/assets/javascripts/discourse/app/widgets/user-status-bubble.js @@ -8,7 +8,7 @@ export default createWidget("user-status-bubble", { let title = attrs.description; if (attrs.ends_at) { const until = moment - .tz(attrs.ends_at, this.currentUser.timezone) + .tz(attrs.ends_at, this.currentUser.user_option.timezone) .format(I18n.t("dates.long_date_without_year")); title += `\n${I18n.t("until")} ${until}`; } diff --git a/app/assets/javascripts/discourse/config/deprecation-workflow.js b/app/assets/javascripts/discourse/config/deprecation-workflow.js index be92c06b23..f0763859a5 100644 --- a/app/assets/javascripts/discourse/config/deprecation-workflow.js +++ b/app/assets/javascripts/discourse/config/deprecation-workflow.js @@ -23,29 +23,5 @@ globalThis.deprecationWorkflow.config = { handler: "silence", matchId: "ember.built-in-components.legacy-arguments", }, - { - handler: "throw", - matchId: "ember-modifier.use-modify", - }, - { - handler: "throw", - matchId: "ember-modifier.use-destroyables", - }, - { - handler: "throw", - matchId: "ember-modifier.no-args-property", - }, - { - handler: "throw", - matchId: "ember-modifier.no-element-property", - }, - { - handler: "throw", - matchId: "ember-modifier.function-based-options", - }, - { - handler: "throw", - matchId: "ember-modifier.function-based-options", - }, ], }; diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json index ef7546e0f0..92e993498a 100644 --- a/app/assets/javascripts/discourse/package.json +++ b/app/assets/javascripts/discourse/package.json @@ -29,12 +29,12 @@ "@glimmer/syntax": "^0.84.2", "@glimmer/tracking": "^1.1.2", "@popperjs/core": "^2.11.6", - "@uppy/aws-s3": "^2.2.1", - "@uppy/aws-s3-multipart": "^2.4.1", - "@uppy/core": "^2.3.1", - "@uppy/drop-target": "^1.1.3", - "@uppy/utils": "^4.1.0", - "@uppy/xhr-upload": "^2.1.2", + "@uppy/aws-s3": "^3.0.4", + "@uppy/aws-s3-multipart": "^3.1.1", + "@uppy/core": "^3.0.4", + "@uppy/drop-target": "^2.0.1", + "@uppy/utils": "^5.1.1", + "@uppy/xhr-upload": "^3.0.4", "a11y-dialog": "7.5.2", "admin": "1.0.0", "babel-import-util": "^1.3.0", @@ -66,13 +66,13 @@ "ember-exam": "^8.0.0", "ember-export-application-global": "^2.0.1", "ember-load-initializers": "^2.1.1", - "ember-modifier": "^3.2.7", + "ember-modifier": "^4.0.0", "ember-on-resize-modifier": "^1.1.0", "ember-qunit": "^6.0.0", "ember-rfc176-data": "^0.3.17", "ember-source": "~3.28.11", "ember-test-selectors": "^6.0.0", - "eslint": "^8.28.0", + "eslint": "^8.29.0", "eslint-plugin-qunit": "^7.3.4", "handlebars": "^4.7.7", "html-entities": "^2.3.3", @@ -87,11 +87,11 @@ "pretty-text": "1.0.0", "qunit": "^2.19.3", "qunit-dom": "^2.0.0", - "sass": "^1.56.1", + "sass": "^1.56.2", "select-kit": "1.0.0", "sinon": "^15.0.0", "source-map": "^0.7.4", - "terser": "^5.16.0", + "terser": "^5.16.1", "tippy.js": "^6.3.7", "util": "^0.12.5", "virtual-dom": "^2.1.1", diff --git a/app/assets/javascripts/discourse/tests/acceptance/admin-silence-user-test.js b/app/assets/javascripts/discourse/tests/acceptance/admin-silence-user-test.js index 4d0f06f7e9..ae6984c4a7 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/admin-silence-user-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/admin-silence-user-test.js @@ -13,7 +13,7 @@ acceptance("Admin - Silence User", function (needs) { needs.user(); needs.hooks.beforeEach(() => { - const timezone = loggedInUser().timezone; + const timezone = loggedInUser().user_option.timezone; clock = fakeTime("2100-05-03T08:00:00", timezone, true); // Monday morning }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/admin-suspend-user-test.js b/app/assets/javascripts/discourse/tests/acceptance/admin-suspend-user-test.js index 73918cd5e2..5e3c36be94 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/admin-suspend-user-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/admin-suspend-user-test.js @@ -112,7 +112,7 @@ acceptance("Admin - Suspend User - timeframe choosing", function (needs) { needs.user(); needs.hooks.beforeEach(() => { - const timezone = loggedInUser().timezone; + const timezone = loggedInUser().user_option.timezone; clock = fakeTime("2100-05-03T08:00:00", timezone, true); // Monday morning }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/admin-webhooks-test.js b/app/assets/javascripts/discourse/tests/acceptance/admin-webhooks-test.js new file mode 100644 index 0000000000..ce8a1057a0 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/admin-webhooks-test.js @@ -0,0 +1,72 @@ +import { acceptance } from "discourse/tests/helpers/qunit-helpers"; +import { click, currentURL, fillIn, visit } from "@ember/test-helpers"; +import { test } from "qunit"; +import pretender, { + parsePostData, + response, +} from "discourse/tests/helpers/create-pretender"; + +acceptance("Admin - Webhooks", function (needs) { + needs.user(); + + test("adding a webhook", async function (assert) { + pretender.get("/admin/api/web_hooks", () => { + return response({ + web_hooks: [], + total_rows_web_hooks: 0, + load_more_web_hooks: "/admin/api/web_hooks.json?limit=50&offset=50", + extras: { + content_types: [ + { id: 1, name: "application/json" }, + { id: 2, name: "application/x-www-form-urlencoded" }, + ], + default_event_types: [{ id: 2, name: "post" }], + delivery_statuses: [ + { id: 1, name: "inactive" }, + { id: 2, name: "failed" }, + { id: 3, name: "successful" }, + ], + event_types: [ + { id: 1, name: "topic" }, + { id: 2, name: "post" }, + { id: 3, name: "user" }, + { id: 4, name: "group" }, + ], + }, + }); + }); + + pretender.get("/admin/api/web_hook_events/1", () => { + return response({ + web_hook_events: [], + load_more_web_hook_events: + "/admin/api/web_hook_events/1.json?limit=50&offset=50", + total_rows_web_hook_events: 15, + extras: { web_hook_id: 1 }, + }); + }); + + pretender.post("/admin/api/web_hooks", (request) => { + const data = parsePostData(request.requestBody); + assert.strictEqual( + data.web_hook.payload_url, + "https://example.com/webhook" + ); + + return response({ + web_hook: { + id: 1, + // other attrs + }, + }); + }); + + await visit("/admin/api/web_hooks"); + await click(".admin-webhooks__new-button"); + + await fillIn(`[name="payload-url"`, "https://example.com/webhook"); + await click(".admin-webhooks__save-button"); + + assert.strictEqual(currentURL(), "/admin/api/web_hooks/1"); + }); +}); diff --git a/app/assets/javascripts/discourse/tests/acceptance/bookmarks-test.js b/app/assets/javascripts/discourse/tests/acceptance/bookmarks-test.js index e0c08690dd..8a65eda13a 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/bookmarks-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/bookmarks-test.js @@ -170,7 +170,10 @@ acceptance("Bookmarking", function (needs) { await selectKit(".bookmark-option-selector").selectRowByValue(1); await click("#save-bookmark"); - assert.equal(User.current().bookmark_auto_delete_preference, "1"); + assert.equal( + User.currentProp("user_option.bookmark_auto_delete_preference"), + "1" + ); await openEditBookmarkModal(); @@ -243,7 +246,7 @@ acceptance("Bookmarking", function (needs) { test("Editing a bookmark", async function (assert) { await visit("/t/internationalization-localization/280"); - let now = moment.tz(loggedInUser().timezone); + let now = moment.tz(loggedInUser().user_option.timezone); let tomorrow = now.add(1, "day").format("YYYY-MM-DD"); await openBookmarkModal(); await fillIn("input#bookmark-name", "Test name"); @@ -269,7 +272,7 @@ acceptance("Bookmarking", function (needs) { test("Using a post date for the reminder date", async function (assert) { await visit("/t/internationalization-localization/280"); - let postDate = moment.tz("2036-01-15", loggedInUser().timezone); + let postDate = moment.tz("2036-01-15", loggedInUser().user_option.timezone); let postDateFormatted = postDate.format("YYYY-MM-DD"); await openBookmarkModal(); await fillIn("input#bookmark-name", "Test name"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js index 29528b624a..41f650c48a 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js @@ -126,7 +126,7 @@ acceptance("Composer - editor mentions", function (needs) { }); test("shows status on search results when mentioning a user", async function (assert) { - const timezone = loggedInUser().timezone; + const timezone = loggedInUser().user_option.timezone; const now = moment(status.ends_at).add(-1, "hour").format(); clock = fakeTime(now, timezone, true); diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-image-preview-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-image-preview-test.js index 75103a3c9b..c70bd81d92 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-image-preview-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-image-preview-test.js @@ -20,12 +20,12 @@ acceptance("Composer - Image Preview", function (needs) { server.get("/posts/419", () => { return helper.response({ id: 419 }); }); - server.get("/u/is_local_username", () => { + server.get("/composer/mentions", () => { return helper.response({ - valid: [], - valid_groups: ["staff"], - mentionable_groups: [{ name: "staff", user_count: 30 }], - cannot_see: [], + users: [], + user_reasons: {}, + groups: { staff: { user_count: 30 } }, + group_reasons: {}, max_users_notified_per_group_mention: 100, }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-messages-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-messages-test.js index 25f9cae229..8d78ac5114 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-messages-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-messages-test.js @@ -3,7 +3,7 @@ import { exists, query, } from "discourse/tests/helpers/qunit-helpers"; -import { click, triggerKeyEvent, visit } from "@ember/test-helpers"; +import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers"; import { test } from "qunit"; import I18n from "I18n"; @@ -39,3 +39,60 @@ acceptance("Composer - Messages", function (needs) { ); }); }); + +acceptance("Composer - Messages - Cannot see group", function (needs) { + needs.user(); + needs.pretender((server, helper) => { + server.get("/composer/mentions", () => { + return helper.response({ + users: [], + user_reasons: {}, + groups: { + staff: { user_count: 30 }, + staff2: { user_count: 30, notified_count: 10 }, + }, + group_reasons: { staff: "not_allowed", staff2: "some_not_allowed" }, + max_users_notified_per_group_mention: 100, + }); + }); + }); + + test("Shows warning in composer if group hasn't been invited", async function (assert) { + await visit("/t/130"); + await click("button.create"); + assert.ok( + !exists(".composer-popup"), + "composer warning is not shown by default" + ); + await fillIn(".d-editor-input", "Mention @staff"); + assert.ok(exists(".composer-popup"), "shows composer warning message"); + assert.ok( + query(".composer-popup").innerHTML.includes( + I18n.t("composer.cannot_see_group_mention.not_allowed", { + group: "staff", + }) + ), + "warning message has correct body" + ); + }); + + test("Shows warning in composer if group hasn't been invited, but some members have access already", async function (assert) { + await visit("/t/130"); + await click("button.create"); + assert.ok( + !exists(".composer-popup"), + "composer warning is not shown by default" + ); + await fillIn(".d-editor-input", "Mention @staff2"); + assert.ok(exists(".composer-popup"), "shows composer warning message"); + assert.ok( + query(".composer-popup").innerHTML.includes( + I18n.t("composer.cannot_see_group_mention.some_not_allowed", { + group: "staff2", + count: 10, + }) + ), + "warning message has correct body" + ); + }); +}); diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js index b250106bd9..9637fd77d2 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js @@ -70,12 +70,12 @@ acceptance("Composer", function (needs) { server.get("/posts/419", () => { return helper.response({ id: 419 }); }); - server.get("/u/is_local_username", () => { + server.get("/composer/mentions", () => { return helper.response({ - valid: [], - valid_groups: ["staff"], - mentionable_groups: [{ name: "staff", user_count: 30 }], - cannot_see: [], + users: [], + user_reasons: {}, + groups: { staff: { user_count: 30 } }, + group_reasons: {}, max_users_notified_per_group_mention: 100, }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js b/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js index 0cc847176e..ce818cb6fc 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js @@ -191,7 +191,7 @@ acceptance( }); needs.hooks.beforeEach(() => { - const timezone = loggedInUser().timezone; + const timezone = loggedInUser().user_option.timezone; clock = fakeTime("2100-05-03T08:00:00", timezone, true); // Monday morning }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/post-inline-mentions-test.js b/app/assets/javascripts/discourse/tests/acceptance/post-inline-mentions-test.js new file mode 100644 index 0000000000..a6f7035d51 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/post-inline-mentions-test.js @@ -0,0 +1,160 @@ +import { + acceptance, + exists, + publishToMessageBus, + query, +} from "discourse/tests/helpers/qunit-helpers"; +import { visit } from "@ember/test-helpers"; +import { test } from "qunit"; +import { cloneJSON } from "discourse-common/lib/object"; +import topicFixtures from "../fixtures/topic"; +import pretender, { response } from "discourse/tests/helpers/create-pretender"; + +acceptance("Post inline mentions test", function (needs) { + needs.user(); + + const topicId = 130; + const mentionedUserId = 1; + const status = { + description: "Surfing", + emoji: "surfing_man", + ends_at: null, + }; + + function topicWithoutUserStatus() { + const topic = cloneJSON(topicFixtures[`/t/${topicId}.json`]); + const firstPost = topic.post_stream.posts[0]; + firstPost.cooked = + '

    I am mentioning @user1 again.

    '; + firstPost.mentioned_users = [ + { + id: mentionedUserId, + username: "user1", + avatar_template: "/letter_avatar_proxy/v4/letter/a/bbce88/{size}.png", + }, + ]; + return topic; + } + + function topicWithUserStatus() { + const topic = topicWithoutUserStatus(); + topic.post_stream.posts[0].mentioned_users[0].status = status; + return topic; + } + + test("shows user status on inline mentions", async function (assert) { + pretender.get(`/t/${topicId}.json`, () => { + return response(topicWithUserStatus()); + }); + + await visit(`/t/lorem-ipsum-dolor-sit-amet/${topicId}`); + + assert.ok( + exists(".topic-post .cooked .mention .user-status"), + "user status is shown" + ); + const statusElement = query(".topic-post .cooked .mention .user-status"); + assert.equal( + statusElement.title, + status.description, + "status description is correct" + ); + assert.ok( + statusElement.src.includes(status.emoji), + "status emoji is correct" + ); + }); + + test("inserts user status on message bus message", async function (assert) { + pretender.get(`/t/${topicId}.json`, () => { + return response(topicWithoutUserStatus()); + }); + await visit(`/t/lorem-ipsum-dolor-sit-amet/${topicId}`); + + assert.notOk( + exists(".topic-post .cooked .mention .user-status"), + "user status isn't shown" + ); + + await publishToMessageBus("/user-status", { + [mentionedUserId]: { + description: status.description, + emoji: status.emoji, + }, + }); + + assert.ok( + exists(".topic-post .cooked .mention .user-status"), + "user status is shown" + ); + const statusElement = query(".topic-post .cooked .mention .user-status"); + assert.equal( + statusElement.title, + status.description, + "status description is correct" + ); + assert.ok( + statusElement.src.includes(status.emoji), + "status emoji is correct" + ); + }); + + test("updates user status on message bus message", async function (assert) { + pretender.get(`/t/${topicId}.json`, () => { + return response(topicWithUserStatus()); + }); + await visit(`/t/lorem-ipsum-dolor-sit-amet/${topicId}`); + + assert.ok( + exists(".topic-post .cooked .mention .user-status"), + "initial user status is shown" + ); + + const newStatus = { + description: "off to dentist", + emoji: "tooth", + }; + await publishToMessageBus("/user-status", { + [mentionedUserId]: { + description: newStatus.description, + emoji: newStatus.emoji, + }, + }); + + assert.ok( + exists(".topic-post .cooked .mention .user-status"), + "updated user status is shown" + ); + const statusElement = query(".topic-post .cooked .mention .user-status"); + assert.equal( + statusElement.title, + newStatus.description, + "updated status description is correct" + ); + assert.ok( + statusElement.src.includes(newStatus.emoji), + "updated status emoji is correct" + ); + }); + + test("removes user status on message bus message", async function (assert) { + pretender.get(`/t/${topicId}.json`, () => { + return response(topicWithUserStatus()); + }); + await visit(`/t/lorem-ipsum-dolor-sit-amet/${topicId}`); + + assert.ok( + exists(".topic-post .cooked .mention .user-status"), + "initial user status is shown" + ); + + await publishToMessageBus("/user-status", { + [mentionedUserId]: null, + }); + + assert.notOk( + exists(".topic-post .cooked .mention .user-status"), + "updated user has disappeared" + ); + }); +}); diff --git a/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js b/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js index 6f60d307d7..9b6e3089cb 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js @@ -111,11 +111,6 @@ acceptance("User Preferences", function (needs) { "apps tab isn't there when you have no authorized apps" ); }); - - test("username", async function (assert) { - await visit("/u/eviltrout/preferences/username"); - assert.ok(exists("#change_username"), "it has the input element"); - }); }); acceptance("Custom User Fields", function (needs) { diff --git a/app/assets/javascripts/discourse/tests/acceptance/redirect-to-top-test.js b/app/assets/javascripts/discourse/tests/acceptance/redirect-to-top-test.js index ac40d43db0..15e453e82e 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/redirect-to-top-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/redirect-to-top-test.js @@ -19,10 +19,12 @@ acceptance("Redirect to Top", function (needs) { test("redirects categories to weekly top", async function (assert) { updateCurrentUser({ - should_be_redirected_to_top: true, - redirected_to_top: { - period: "weekly", - reason: "Welcome back!", + user_option: { + should_be_redirected_to_top: true, + redirected_to_top: { + period: "weekly", + reason: "Welcome back!", + }, }, }); @@ -36,10 +38,12 @@ acceptance("Redirect to Top", function (needs) { test("redirects latest to monthly top", async function (assert) { updateCurrentUser({ - should_be_redirected_to_top: true, - redirected_to_top: { - period: "monthly", - reason: "Welcome back!", + user_option: { + should_be_redirected_to_top: true, + redirected_to_top: { + period: "monthly", + reason: "Welcome back!", + }, }, }); @@ -53,10 +57,12 @@ acceptance("Redirect to Top", function (needs) { test("redirects root to All top", async function (assert) { updateCurrentUser({ - should_be_redirected_to_top: true, - redirected_to_top: { - period: null, - reason: "Welcome back!", + user_option: { + should_be_redirected_to_top: true, + redirected_to_top: { + period: null, + reason: "Welcome back!", + }, }, }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js b/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js index 0d8c4cc0c6..a1a04511eb 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js @@ -44,12 +44,6 @@ const RESPONSES = { security_keys_enabled: true, allowed_methods: [BACKUP_CODE], }, - ok010010: { - totp_enabled: false, - backup_enabled: true, - security_keys_enabled: false, - allowed_methods: [BACKUP_CODE], - }, }; Object.keys(RESPONSES).forEach((k) => { @@ -184,14 +178,6 @@ acceptance("Second Factor Auth Page", function (needs) { !exists(".toggle-second-factor-method"), "no alternative methods are shown if only 1 method is allowed" ); - - // only backup codes - await visit("/session/2fa?nonce=ok010010"); - assert.ok(exists("form.backup-code-token"), "backup code form is shown"); - assert.ok( - !exists(".toggle-second-factor-method"), - "no alternative methods are shown if only 1 method is allowed" - ); }); test("switching 2FA methods", async function (assert) { @@ -299,8 +285,7 @@ acceptance("Second Factor Auth Page", function (needs) { }); test("sidebar is disabled on 2FA route", async function (assert) { - this.siteSettings.enable_experimental_sidebar_hamburger = true; - this.siteSettings.enable_sidebar = true; + this.siteSettings.navigation_menu = "sidebar"; await visit("/session/2fa?nonce=ok110111"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-categories-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-categories-section-test.js index 9ec6b59826..252a8e756b 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-categories-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-categories-section-test.js @@ -9,8 +9,7 @@ import Site from "discourse/models/site"; acceptance("Sidebar - Anonymous - Categories Section", function (needs) { needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); test("category section links ordered by category's topic count when default_sidebar_categories has not been configured and site setting to fix categories positions is disabled", async function (assert) { diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-community-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-community-section-test.js index c01d2357d9..28d186a8f5 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-community-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-community-section-test.js @@ -12,8 +12,7 @@ import { click, visit } from "@ember/test-helpers"; acceptance("Sidebar - Anonymous user - Community Section", function (needs) { needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); test("display short site description site setting when it is set", async function (assert) { diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-tags-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-tags-section-test.js index 09d805069b..715f08aa5f 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-tags-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-tags-section-test.js @@ -9,8 +9,7 @@ import Site from "discourse/models/site"; acceptance("Sidebar - Anonymous Tags Section", function (needs) { needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", suppress_uncategorized_badge: false, tagging_enabled: true, }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-user-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-user-test.js index d350dd1aaa..f6de25ef9d 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-user-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-user-test.js @@ -6,8 +6,7 @@ import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers"; acceptance("Sidebar - Anonymous User", function (needs) { needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); test("sidebar is displayed", async function (assert) { @@ -30,7 +29,7 @@ acceptance("Sidebar - Anonymous User", function (needs) { }); test("sidebar hamburger panel dropdown when sidebar has been disabled", async function (assert) { - this.siteSettings.enable_sidebar = false; + this.siteSettings.navigation_menu = "header dropdown"; await visit("/"); await click(".hamburger-dropdown"); @@ -44,8 +43,7 @@ acceptance("Sidebar - Anonymous User", function (needs) { acceptance("Sidebar - Anonymous User - Login Required", function (needs) { needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", login_required: true, }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-mobile-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-mobile-test.js index 3241278e81..071a1bb5cc 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-mobile-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-mobile-test.js @@ -9,8 +9,7 @@ acceptance("Sidebar - Mobile - User with sidebar enabled", function (needs) { needs.user(); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); needs.mobileView(); diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-narrow-desktop-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-narrow-desktop-test.js index 60d1f38184..7ac9ef275f 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-narrow-desktop-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-narrow-desktop-test.js @@ -8,8 +8,7 @@ acceptance("Sidebar - Narrow Desktop", function (needs) { needs.user(); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); test("wide sidebar is changed to cloak when resize to narrow screen", async function (assert) { @@ -55,4 +54,30 @@ acceptance("Sidebar - Narrow Desktop", function (needs) { bodyElement.style.width = null; }); + + test("transition from narrow screen to wide screen", async function (assert) { + await visit("/"); + await settled(); + + const bodyElement = document.querySelector("body"); + bodyElement.style.width = "990px"; + + await waitUntil( + () => document.querySelector(".btn-sidebar-toggle.narrow-desktop"), + { + timeout: 5000, + } + ); + await click(".btn-sidebar-toggle"); + + bodyElement.style.width = "1200px"; + await waitUntil(() => document.querySelector("#d-sidebar"), { + timeout: 5000, + }); + await click(".header-dropdown-toggle.current-user"); + $(".header-dropdown-toggle.current-user").click(); + assert.ok(exists(".quick-access-panel")); + + bodyElement.style.width = null; + }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js index 511f6f1b29..489972f675 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js @@ -14,8 +14,7 @@ acceptance("Sidebar - Plugin API", function (needs) { needs.user(); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); needs.hooks.afterEach(() => { diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-categories-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-categories-section-test.js index 3b4a14cd4c..dd182b99b9 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-categories-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-categories-section-test.js @@ -22,8 +22,7 @@ acceptance( function (needs) { needs.settings({ allow_uncategorized_topics: false, - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); needs.user({ admin: false }); @@ -66,8 +65,7 @@ acceptance("Sidebar - Logged on user - Categories Section", function (needs) { }); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", suppress_uncategorized_badge: false, allow_uncategorized_topics: true, }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js index 47c505d2f3..41756649fe 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js @@ -28,8 +28,7 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) { }); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); needs.pretender((server, helper) => { diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-messages-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-messages-section-test.js index fb7c120e10..6ae2256161 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-messages-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-messages-section-test.js @@ -22,8 +22,7 @@ acceptance( }); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); test("clicking on section header button", async function (assert) { @@ -43,8 +42,7 @@ acceptance( needs.user({ can_send_private_messages: true }); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); needs.pretender((server, helper) => { diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-tags-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-tags-section-test.js index 7b80114b9c..018d7d3b23 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-tags-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-tags-section-test.js @@ -19,8 +19,7 @@ acceptance( function (needs) { needs.settings({ tagging_enabled: false, - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); needs.user(); @@ -39,8 +38,7 @@ acceptance( acceptance("Sidebar - Logged on user - Tags section", function (needs) { needs.settings({ tagging_enabled: true, - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); needs.user({ diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-test.js index 8f80d85dc3..ed2b4c5db3 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-test.js @@ -14,7 +14,7 @@ acceptance( needs.user(); needs.settings({ - enable_experimental_sidebar_hamburger: false, + navigation_menu: "legacy", }); test("clicking header hamburger icon displays old hamburger dropdown", async function (assert) { @@ -32,8 +32,7 @@ acceptance( needs.user(); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: false, + navigation_menu: "header dropdown", }); test("showing and hiding sidebar", async function (assert) { @@ -80,8 +79,7 @@ acceptance( needs.user(); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); test("viewing keyboard shortcuts using sidebar", async function (assert) { diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-bulk-actions-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-bulk-actions-test.js index 9ff9d542db..0193efa000 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-bulk-actions-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-bulk-actions-test.js @@ -23,7 +23,10 @@ acceptance("Topic - Bulk Actions", function (needs) { }); test("bulk select - modal", async function (assert) { - updateCurrentUser({ moderator: true, enable_defer: true }); + updateCurrentUser({ + moderator: true, + user_option: { enable_defer: true }, + }); await visit("/latest"); await click("button.bulk-select"); @@ -168,7 +171,13 @@ acceptance("Topic - Bulk Actions", function (needs) { }); test("TL4 users can bulk select", async function (assert) { - updateCurrentUser({ moderator: false, admin: false, trust_level: 4 }); + updateCurrentUser({ + moderator: false, + admin: false, + trust_level: 4, + user_option: { enable_defer: false }, + }); + await visit("/latest"); await click("button.bulk-select"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-edit-timer-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-edit-timer-test.js index 39d77bdd8b..728de641e5 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-edit-timer-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-edit-timer-test.js @@ -11,6 +11,8 @@ import { click, fillIn, visit } from "@ember/test-helpers"; import { test } from "qunit"; import selectKit from "discourse/tests/helpers/select-kit-helper"; import I18n from "I18n"; +import { cloneJSON } from "discourse-common/lib/object"; +import topicFixtures from "discourse/tests/fixtures/topic"; acceptance("Topic - Edit timer", function (needs) { let clock = null; @@ -28,10 +30,14 @@ acceptance("Topic - Edit timer", function (needs) { category_id: null, }) ); + + const topicResponse = cloneJSON(topicFixtures["/t/54077.json"]); + topicResponse.details.can_delete = false; + server.get("/t/54077.json", () => helper.response(topicResponse)); }); needs.hooks.beforeEach(() => { - const timezone = loggedInUser().timezone; + const timezone = loggedInUser().user_option.timezone; const tuesday = "2100-06-15T08:00:00"; clock = fakeTime(tuesday, timezone, true); }); @@ -294,7 +300,7 @@ acceptance("Topic - Edit timer", function (needs) { test("TL4 can't auto-delete", async function (assert) { updateCurrentUser({ moderator: false, admin: false, trust_level: 4 }); - await visit("/t/internationalization-localization"); + await visit("/t/short-topic-with-two-posts/54077"); await click(".toggle-admin-menu"); await click(".admin-topic-timer-update button"); @@ -305,6 +311,48 @@ acceptance("Topic - Edit timer", function (needs) { assert.ok(!timerType.rowByValue("delete").exists()); }); + test("Category Moderator can auto-delete replies", async function (assert) { + updateCurrentUser({ moderator: false, admin: false, trust_level: 4 }); + + await visit("/t/internationalization-localization"); + await click(".toggle-admin-menu"); + await click(".admin-topic-timer-update button"); + + const timerType = selectKit(".select-kit.timer-type"); + + await timerType.expand(); + + assert.ok(timerType.rowByValue("delete_replies").exists()); + }); + + test("TL4 can't auto-delete replies", async function (assert) { + updateCurrentUser({ moderator: false, admin: false, trust_level: 4 }); + + await visit("/t/short-topic-with-two-posts/54077"); + await click(".toggle-admin-menu"); + await click(".admin-topic-timer-update button"); + + const timerType = selectKit(".select-kit.timer-type"); + + await timerType.expand(); + + assert.ok(!timerType.rowByValue("delete_replies").exists()); + }); + + test("Category Moderator can auto-delete", async function (assert) { + updateCurrentUser({ moderator: false, admin: false, trust_level: 4 }); + + await visit("/t/internationalization-localization"); + await click(".toggle-admin-menu"); + await click(".admin-topic-timer-update button"); + + const timerType = selectKit(".select-kit.timer-type"); + + await timerType.expand(); + + assert.ok(timerType.rowByValue("delete").exists()); + }); + test("auto delete", async function (assert) { updateCurrentUser({ moderator: true }); const timerType = selectKit(".select-kit.timer-type"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-set-slow-mode-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-set-slow-mode-test.js index e35eb72071..890dc80b74 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-set-slow-mode-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-set-slow-mode-test.js @@ -29,7 +29,7 @@ acceptance("Topic - Set Slow Mode", function (needs) { }); needs.hooks.beforeEach(() => { - const timezone = loggedInUser().timezone; + const timezone = loggedInUser().user_option.timezone; clock = fakeTime("2100-05-03T08:00:00", timezone, true); // Monday morning }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js index bfd7611d4a..43904e65b7 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js @@ -12,11 +12,6 @@ import { cloneJSON } from "discourse-common/lib/object"; acceptance("User Card - Show Local Time", function (needs) { needs.user(); needs.settings({ display_local_time_in_user_card: true }); - needs.pretender((server, helper) => { - const cardResponse = cloneJSON(userFixtures["/u/charlie/card.json"]); - delete cardResponse.user.timezone; - server.get("/u/charlie/card.json", () => helper.response(cardResponse)); - }); test("user card local time - does not update timezone for another user", async function (assert) { User.current().timezone = "Australia/Brisbane"; diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-account-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-account-test.js index e33b9ccb45..83d8162c6c 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-account-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-account-test.js @@ -1,14 +1,17 @@ import { test } from "qunit"; import I18n from "I18n"; import sinon from "sinon"; -import { click, visit } from "@ember/test-helpers"; +import { click, fillIn, visit } from "@ember/test-helpers"; import { acceptance, exists, query, } from "discourse/tests/helpers/qunit-helpers"; import DiscourseURL from "discourse/lib/url"; -import { fixturesByUrl } from "discourse/tests/helpers/create-pretender"; +import pretender, { + fixturesByUrl, + response, +} from "discourse/tests/helpers/create-pretender"; import { cloneJSON } from "discourse-common/lib/object"; acceptance("User Preferences - Account", function (needs) { @@ -58,6 +61,41 @@ acceptance("User Preferences - Account", function (needs) { pickAvatarRequestData = null; }); + test("changing username", async function (assert) { + const stub = sinon + .stub(DiscourseURL, "redirectTo") + .withArgs("/u/good_trout/preferences"); + + pretender.put("/u/eviltrout/preferences/username", (data) => { + assert.strictEqual(data.requestBody, "new_username=good_trout"); + + return response({ + id: fixturesByUrl["/u/eviltrout.json"].user.id, + username: "good_trout", + }); + }); + + await visit("/u/eviltrout/preferences/account"); + + assert.strictEqual( + query(".username-preference__current-username").innerText, + "eviltrout" + ); + + await click(".username-preference__edit-username"); + + assert.strictEqual(query(".username-preference__input").value, "eviltrout"); + assert.true(query(".username-preference__submit").disabled); + + await fillIn(query(".username-preference__input"), "good_trout"); + assert.false(query(".username-preference__submit").disabled); + + await click(".username-preference__submit"); + await click(".dialog-container .btn-primary"); + + sinon.assert.calledOnce(stub); + }); + test("Delete dialog", async function (assert) { sinon.stub(DiscourseURL, "redirectAbsolute"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-notifications-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-notifications-test.js index d107673ac2..b4f1996ba9 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-notifications-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-notifications-test.js @@ -116,7 +116,7 @@ acceptance("User Notifications - Users - Ignore User", function (needs) { needs.user(); needs.hooks.beforeEach(() => { - const timezone = loggedInUser().timezone; + const timezone = loggedInUser().user_option.timezone; clock = fakeTime("2100-05-03T08:00:00", timezone, true); // Monday morning }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-profile-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-profile-test.js index 1bf8cfa1f4..344ff4b607 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-profile-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-profile-test.js @@ -5,6 +5,8 @@ import { } from "discourse/tests/helpers/qunit-helpers"; import { click, visit } from "@ember/test-helpers"; import { test } from "qunit"; +import { cloneJSON } from "discourse-common/lib/object"; +import userFixtures from "discourse/tests/fixtures/user-fixtures"; acceptance("User - Preferences - Profile - Featured topic", function (needs) { needs.user(); @@ -65,7 +67,15 @@ acceptance("User - Preferences - Profile - Featured topic", function (needs) { acceptance( "User - Preferences - Profile - No default calendar set", function (needs) { - needs.user({ default_calendar: "none_selected" }); + needs.user(); + + needs.pretender((server, helper) => { + server.get("/u/eviltrout.json", () => { + const cloned = cloneJSON(userFixtures["/u/eviltrout.json"]); + cloned.user.user_option.default_calendar = "none_selected"; + return helper.response(200, cloned); + }); + }); test("default calendar option is not visible", async function (assert) { await visit("/u/eviltrout/preferences/profile"); @@ -81,7 +91,15 @@ acceptance( acceptance( "User - Preferences - Profile - Default calendar set", function (needs) { - needs.user({ default_calendar: "google" }); + needs.user(); + + needs.pretender((server, helper) => { + server.get("/u/eviltrout.json", () => { + const cloned = cloneJSON(userFixtures["/u/eviltrout.json"]); + cloned.user.user_option.default_calendar = "google"; + return helper.response(200, cloned); + }); + }); test("default calendar can be changed", async function (assert) { await visit("/u/eviltrout/preferences/profile"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-backup-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-backup-test.js index 218823c676..ebd73c0bc4 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-backup-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-backup-test.js @@ -3,6 +3,7 @@ import { click, visit } from "@ember/test-helpers"; import { acceptance, exists, + query, updateCurrentUser, } from "discourse/tests/helpers/qunit-helpers"; @@ -42,4 +43,17 @@ acceptance("User Preferences - Second Factor Backup", function (needs) { assert.ok(exists(".backup-codes-area"), "shows backup codes"); }); + + test("delete backup codes", async function (assert) { + updateCurrentUser({ second_factor_enabled: true }); + await visit("/u/eviltrout/preferences/second-factor"); + await click(".edit-2fa-backup"); + await click(".second-factor-backup-preferences .btn-primary"); + await click(".modal-close"); + await click(".pref-second-factor-backup .btn-danger"); + assert.strictEqual( + query("#dialog-title").innerText.trim(), + "Deleting backup codes" + ); + }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-test.js index d5b9a2630e..6f8a1dc810 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-test.js @@ -4,6 +4,7 @@ import { acceptance, exists, query, + updateCurrentUser, } from "discourse/tests/helpers/qunit-helpers"; acceptance("User Preferences - Second Factor", function (needs) { @@ -14,6 +15,8 @@ acceptance("User Preferences - Second Factor", function (needs) { return helper.response({ success: "OK", password_required: "true", + totps: [{ id: 1, name: "one of them" }], + security_keys: [{ id: 2, name: "key" }], }); }); @@ -90,4 +93,33 @@ acceptance("User Preferences - Second Factor", function (needs) { ); } }); + + test("delete second factor security method", async function (assert) { + updateCurrentUser({ moderator: false, admin: false, trust_level: 1 }); + await visit("/u/eviltrout/preferences/second-factor"); + + assert.ok(exists("#password"), "it has a password input"); + + await fillIn("#password", "secrets"); + await click(".user-preferences .btn-primary"); + await click(".totp .btn-danger"); + assert.strictEqual( + query("#dialog-title").innerText.trim(), + "Deleting an authenticator" + ); + await click(".dialog-close"); + + await click(".security-key .btn-danger"); + assert.strictEqual( + query("#dialog-title").innerText.trim(), + "Deleting an authenticator" + ); + await click(".dialog-close"); + + await click(".btn-danger.btn-icon-text"); + assert.strictEqual( + query("#dialog-title").innerText.trim(), + "Are you sure you want to disable two-factor authentication?" + ); + }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-sidebar-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-sidebar-test.js index 85db3fbb62..02eb2773dd 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-sidebar-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-sidebar-test.js @@ -17,8 +17,7 @@ acceptance("User Preferences - Sidebar", function (needs) { }); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", tagging_enabled: true, }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-tracking-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-tracking-test.js index 92016c8e37..81be6acbbf 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-tracking-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-tracking-test.js @@ -189,6 +189,8 @@ acceptance("User Preferences - Tracking", function (needs) { await click(".save-changes"); assert.deepEqual(putRequestData, { + auto_track_topics_after_msecs: "60000", + new_topic_duration_minutes: "1440", "regular_category_ids[]": ["-1"], "tracked_category_ids[]": ["4"], "watched_category_ids[]": ["3"], @@ -211,6 +213,8 @@ acceptance("User Preferences - Tracking", function (needs) { await click(".save-changes"); assert.deepEqual(putRequestData, { + auto_track_topics_after_msecs: "60000", + new_topic_duration_minutes: "1440", "muted_category_ids[]": ["-1"], "tracked_category_ids[]": ["4"], "watched_category_ids[]": ["3"], diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-private-messages-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-private-messages-test.js index 8dc4313df4..9ac356da93 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-private-messages-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-private-messages-test.js @@ -17,6 +17,8 @@ import { resetHighestReadCache, setHighestReadCache, } from "discourse/lib/topic-list-tracker"; +import { withPluginApi } from "discourse/lib/plugin-api"; +import { resetCustomUserNavMessagesDropdownRows } from "discourse/controllers/user-private-messages"; acceptance( "User Private Messages - user with no group messages", @@ -753,6 +755,30 @@ function testUserPrivateMessagesWithGroupMessages(needs, customUserProps) { "All tags is still selected in dropdown" ); }); + + test("addUserMessagesNavigationDropdownRow plugin api", async function (assert) { + try { + withPluginApi("1.5.0", (api) => { + api.addUserMessagesNavigationDropdownRow( + "preferences", + "test nav", + "arrow-left" + ); + }); + + await visit("/u/eviltrout/messages"); + + const messagesDropdown = selectKit(".user-nav-messages-dropdown"); + await messagesDropdown.expand(); + + const row = messagesDropdown.rowByName("test nav"); + + assert.strictEqual(row.value(), "/u/eviltrout/preferences"); + assert.ok(row.icon().classList.contains("d-icon-arrow-left")); + } finally { + resetCustomUserNavMessagesDropdownRows(); + } + }); } } diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-status-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-status-test.js index ccef190ca1..9e8e0d14c5 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-status-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-status-test.js @@ -26,7 +26,7 @@ acceptance("User Status", function (needs) { const userId = 1; const userTimezone = "UTC"; - needs.user({ id: userId, timezone: userTimezone }); + needs.user({ id: userId, "user_option.timezone": userTimezone }); needs.pretender((server, helper) => { server.put("/user-status.json", () => { diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-test.js index c84da617f8..e8ce1fad39 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-test.js @@ -1,6 +1,5 @@ import I18n from "I18n"; import EmberObject from "@ember/object"; -import User from "discourse/models/user"; import selectKit from "discourse/tests/helpers/select-kit-helper"; import sinon from "sinon"; import userFixtures from "discourse/tests/fixtures/user-fixtures"; @@ -167,21 +166,33 @@ acceptance("User - Saving user options", function (needs) { disable_mailing_list_mode: false, }); + let putRequestData; + needs.pretender((server, helper) => { - server.put("/u/eviltrout.json", () => { - return helper.response(200, { user: {} }); + server.put("/u/eviltrout.json", (request) => { + putRequestData = helper.parsePostData(request.requestBody); + return helper.response({ user: {} }); }); }); - test("saving user options", async function (assert) { - const spy = sinon.spy(User.current(), "_saveUserData"); + needs.hooks.afterEach(() => { + putRequestData = null; + }); + test("saving user options", async function (assert) { await visit("/u/eviltrout/preferences/emails"); await click(".pref-mailing-list-mode input[type='checkbox']"); await click(".save-changes"); - assert.ok( - spy.calledWithMatch({ mailing_list_mode: true }), + assert.deepEqual( + putRequestData, + { + digest_after_minutes: "10080", + email_digests: "true", + email_level: "1", + email_messages_level: "0", + mailing_list_mode: "true", + }, "sends a PUT request to update the specified user option" ); @@ -189,8 +200,15 @@ acceptance("User - Saving user options", function (needs) { await selectKit("#user-email-messages-level").selectRowByValue(2); // never option await click(".save-changes"); - assert.ok( - spy.calledWithMatch({ email_messages_level: 2 }), + assert.deepEqual( + putRequestData, + { + digest_after_minutes: "10080", + email_digests: "true", + email_level: "1", + email_messages_level: "2", + mailing_list_mode: "true", + }, "is able to save a different user_option on a subsequent request" ); }); diff --git a/app/assets/javascripts/discourse/tests/fixtures/session-fixtures.js b/app/assets/javascripts/discourse/tests/fixtures/session-fixtures.js index b5430580e8..fe0c2dc870 100644 --- a/app/assets/javascripts/discourse/tests/fixtures/session-fixtures.js +++ b/app/assets/javascripts/discourse/tests/fixtures/session-fixtures.js @@ -18,21 +18,14 @@ export default { title: "co-founder", reply_count: 859, topic_count: 36, - enable_quoting: true, - external_links_in_new_tab: false, - dynamic_favicon: true, trust_level: 4, can_edit: true, can_invite_to_forum: true, can_send_private_messages: true, - should_be_redirected_to_top: false, custom_fields: {}, muted_category_ids: [], dismissed_banner_key: null, akismet_review_count: 0, - title_count_mode: "notifications", - timezone: "Australia/Brisbane", - skip_new_user_tips: false, can_review: true, ignored_users: [], groups: [ @@ -48,7 +41,16 @@ export default { name: "trust_level_1", display_name: "trust_level_1", } - ] + ], + user_option: { + external_links_in_new_tab: false, + enable_quoting: true, + dynamic_favicon: true, + title_count_mode: "notifications", + timezone: "Australia/Brisbane", + skip_new_user_tips: false, + should_be_redirected_to_top: false, + }, }, }, }; diff --git a/app/assets/javascripts/discourse/tests/fixtures/user-fixtures.js b/app/assets/javascripts/discourse/tests/fixtures/user-fixtures.js index 471f7c47fc..979543af5d 100644 --- a/app/assets/javascripts/discourse/tests/fixtures/user-fixtures.js +++ b/app/assets/javascripts/discourse/tests/fixtures/user-fixtures.js @@ -107,6 +107,18 @@ export default { user: { user_option: { text_size_seq: 1, + email_digests: true, + email_messages_level: 0, + email_level: 1, + digest_after_minutes: 10080, + mailing_list_mode: false, + auto_track_topics_after_msecs: 60000, + new_topic_duration_minutes: 1440, + external_links_in_new_tab: false, + dynamic_favicon: true, + skip_new_user_tips: false, + enable_quoting: true, + timezone: "Australia/Brisbane", }, id: 19, username: "eviltrout", @@ -166,17 +178,6 @@ export default { can_be_deleted: false, can_delete_all_posts: false, locale: "", - email_digests: true, - email_messages_level: 0, - email_level: 1, - digest_after_minutes: 10080, - mailing_list_mode: false, - auto_track_topics_after_msecs: 60000, - new_topic_duration_minutes: 1440, - external_links_in_new_tab: false, - dynamic_favicon: true, - skip_new_user_tips: false, - enable_quoting: true, muted_category_ids: [], regular_category_ids: [4], tracked_category_ids: [], @@ -294,7 +295,6 @@ export default { day_6_start_time: 480, day_6_end_time: 1020, }, - timezone: "Australia/Brisbane", has_topic_draft: true, }, }, @@ -2722,8 +2722,8 @@ export default { hide_profile_and_presence: false, text_size: "normal", text_size_seq: 0, + timezone: "America/Los_Angeles", }, - timezone: "America/Los_Angeles", }, }, "/u/charlie/card.json": { @@ -3253,6 +3253,18 @@ export default { user: { user_option: { text_size_seq: 1, + email_digests: true, + email_messages_level: 0, + email_level: 1, + digest_after_minutes: 10080, + mailing_list_mode: false, + auto_track_topics_after_msecs: 60000, + new_topic_duration_minutes: 1440, + external_links_in_new_tab: false, + dynamic_favicon: true, + skip_new_user_tips: false, + enable_quoting: true, + timezone: "Australia/Brisbane", }, id: 4432, username: "e.il.rout", @@ -3312,17 +3324,6 @@ export default { can_be_deleted: false, can_delete_all_posts: false, locale: "", - email_digests: true, - email_messages_level: 0, - email_level: 1, - digest_after_minutes: 10080, - mailing_list_mode: false, - auto_track_topics_after_msecs: 60000, - new_topic_duration_minutes: 1440, - external_links_in_new_tab: false, - dynamic_favicon: true, - skip_new_user_tips: false, - enable_quoting: true, muted_category_ids: [], regular_category_ids: [], tracked_category_ids: [], @@ -3439,7 +3440,6 @@ export default { day_6_start_time: 480, day_6_end_time: 1020, }, - timezone: "Australia/Brisbane", }, }, "/u/staged.json": { @@ -3470,6 +3470,18 @@ export default { user: { user_option: { text_size_seq: 1, + email_digests: false, + email_messages_level: 0, + email_level: 1, + digest_after_minutes: 10080, + mailing_list_mode: false, + auto_track_topics_after_msecs: 60000, + new_topic_duration_minutes: 1440, + external_links_in_new_tab: false, + dynamic_favicon: true, + skip_new_user_tips: false, + enable_quoting: true, + timezone: "Australia/Brisbane", }, id: 20, username: "staged", @@ -3509,17 +3521,6 @@ export default { can_be_deleted: true, can_delete_all_posts: true, locale: "", - email_digests: false, - email_messages_level: 0, - email_level: 1, - digest_after_minutes: 10080, - mailing_list_mode: false, - auto_track_topics_after_msecs: 60000, - new_topic_duration_minutes: 1440, - external_links_in_new_tab: false, - dynamic_favicon: true, - skip_new_user_tips: false, - enable_quoting: true, muted_category_ids: [], regular_category_ids: [], tracked_category_ids: [], @@ -3562,7 +3563,6 @@ export default { day_6_start_time: 480, day_6_end_time: 1020, }, - timezone: "Australia/Brisbane", }, }, "/u/recent-searches": { diff --git a/app/assets/javascripts/discourse/tests/helpers/component-test.js b/app/assets/javascripts/discourse/tests/helpers/component-test.js index 4e79dd558a..f4e36d4c98 100644 --- a/app/assets/javascripts/discourse/tests/helpers/component-test.js +++ b/app/assets/javascripts/discourse/tests/helpers/component-test.js @@ -24,7 +24,6 @@ export function setupRenderingTest(hooks) { const currentUser = User.create({ username: "eviltrout", - timezone: "Australia/Brisbane", name: "Robin Ward", admin: false, moderator: false, @@ -42,6 +41,9 @@ export function setupRenderingTest(hooks) { display_name: "trust_level_1", }, ], + user_option: { + timezone: "Australia/Brisbane", + }, }); this.currentUser = currentUser; this.owner.unregister("service:current-user"); diff --git a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js index da7ab2e5f7..d694826cf5 100644 --- a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js +++ b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js @@ -173,12 +173,13 @@ export function applyDefaultHandlers(pretender) { return response({ email: "eviltrout@example.com" }); }); - pretender.get("/u/is_local_username", () => + pretender.get("/composer/mentions", () => response({ - valid: [], - valid_groups: [], - mentionable_groups: [], - cannot_see: [], + users: [], + user_reasons: {}, + groups: {}, + group_reasons: {}, + max_users_notified_per_group_mention: 100, }) ); diff --git a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js index 06b4062601..b4d49f2f6d 100644 --- a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js +++ b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js @@ -75,6 +75,7 @@ import { resetSidebarSection } from "discourse/lib/sidebar/custom-sections"; import { resetNotificationTypeRenderers } from "discourse/lib/notification-types-manager"; import { resetUserMenuTabs } from "discourse/lib/user-menu/tab"; import { reset as resetLinkLookup } from "discourse/lib/link-lookup"; +import { resetMentions } from "discourse/lib/link-mentions"; import { resetModelTransformers } from "discourse/lib/model-transformers"; import { cleanupTemporaryModuleRegistrations } from "./temporary-module-helper"; @@ -208,6 +209,7 @@ export function testCleanup(container, app) { resetUserMenuTabs(); resetLinkLookup(); resetModelTransformers(); + resetMentions(); cleanupTemporaryModuleRegistrations(); } diff --git a/app/assets/javascripts/discourse/tests/integration/components/bookmark-icon-test.js b/app/assets/javascripts/discourse/tests/integration/components/bookmark-icon-test.js index eeb58665f2..5d277512ce 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/bookmark-icon-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/bookmark-icon-test.js @@ -14,7 +14,7 @@ module("Integration | Component | bookmark-icon", function (hooks) { test("with reminder", async function (assert) { this.setProperties({ bookmark: Bookmark.create({ - reminder_at: tomorrow(this.currentUser.timezone), + reminder_at: tomorrow(this.currentUser.user_option.timezone), name: "some name", }), }); @@ -29,7 +29,7 @@ module("Integration | Component | bookmark-icon", function (hooks) { I18n.t("bookmarks.created_with_reminder_generic", { date: formattedReminderTime( this.bookmark.reminder_at, - this.currentUser.timezone + this.currentUser.user_option.timezone ), name: "some name", }) diff --git a/app/assets/javascripts/discourse/tests/integration/components/composer-editor-test.js b/app/assets/javascripts/discourse/tests/integration/components/composer-editor-test.js index 6bd6016b72..28405f9cd3 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/composer-editor-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/composer-editor-test.js @@ -8,26 +8,28 @@ module("Integration | Component | ComposerEditor", function (hooks) { setupRenderingTest(hooks); test("warns about users that will not see a mention", async function (assert) { - assert.expect(1); + assert.expect(2); this.set("model", {}); this.set("noop", () => {}); - this.set("expectation", (warnings) => { - assert.deepEqual(warnings, [ - { name: "user-no", reason: "a reason" }, - { name: "user-nope", reason: "a reason" }, - ]); + this.set("expectation", (warning) => { + if (warning.name === "user-no") { + assert.deepEqual(warning, { name: "user-no", reason: "a reason" }); + } else if (warning.name === "user-nope") { + assert.deepEqual(warning, { name: "user-nope", reason: "a reason" }); + } }); - pretender.get("/u/is_local_username", () => { + pretender.get("/composer/mentions", () => { return response({ - cannot_see: { + users: ["user-ok", "user-no", "user-nope"], + user_reasons: { "user-no": "a reason", "user-nope": "a reason", }, - mentionable_groups: [], - valid: ["user-ok", "user-no", "user-nope"], - valid_groups: [], + groups: {}, + group_reasons: {}, + max_users_notified_per_group_mention: 100, }); }); diff --git a/app/assets/javascripts/discourse/tests/integration/components/d-document-test.js b/app/assets/javascripts/discourse/tests/integration/components/d-document-test.js index 361c55b570..885818fbdb 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/d-document-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/d-document-test.js @@ -23,7 +23,7 @@ module("Integration | Component | d-document", function (hooks) { const titleBefore = document.title; try { this.currentUser.redesigned_user_menu_enabled = true; - this.currentUser.title_count_mode = "notifications"; + this.currentUser.user_option.title_count_mode = "notifications"; await render(hbs``); assert.strictEqual( getTitleCount(), @@ -51,7 +51,7 @@ module("Integration | Component | d-document", function (hooks) { const titleBefore = document.title; try { this.currentUser.redesigned_user_menu_enabled = false; - this.currentUser.title_count_mode = "notifications"; + this.currentUser.user_option.title_count_mode = "notifications"; await render(hbs``); assert.strictEqual( getTitleCount(), diff --git a/app/assets/javascripts/discourse/tests/integration/components/dialog-holder-test.js b/app/assets/javascripts/discourse/tests/integration/components/dialog-holder-test.js index 251e8ac4e2..a1ed2606e0 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/dialog-holder-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/dialog-holder-test.js @@ -1,7 +1,13 @@ import I18n from "I18n"; import { module, test } from "qunit"; import { setupRenderingTest } from "discourse/tests/helpers/component-test"; -import { click, render, settled, triggerKeyEvent } from "@ember/test-helpers"; +import { + click, + fillIn, + render, + settled, + triggerKeyEvent, +} from "@ember/test-helpers"; import { hbs } from "ember-cli-htmlbars"; import { query } from "discourse/tests/helpers/qunit-helpers"; @@ -388,4 +394,17 @@ module("Integration | Component | dialog-holder", function (hooks) { ".btn-primary element is not present in the dialog" ); }); + test("delete confirm with confirmation phase", async function (assert) { + await render(hbs``); + + this.dialog.deleteConfirm({ + message: "A delete confirm message", + confirmPhrase: "test", + }); + await settled(); + + assert.strictEqual(query(".btn-danger").disabled, true); + await fillIn("#confirm-phrase", "test"); + assert.strictEqual(query(".btn-danger").disabled, false); + }); }); diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/future-date-input-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/future-date-input-test.js index 8d7af274cd..b652adc469 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/future-date-input-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/future-date-input-test.js @@ -58,7 +58,11 @@ module( test("renders default options", async function (assert) { const monday = "2100-12-13T08:00:00"; - this.clock = fakeTime(monday, this.currentUser.timezone, true); + this.clock = fakeTime( + monday, + this.currentUser.user_option.timezone, + true + ); await render(hbs``); diff --git a/app/assets/javascripts/discourse/tests/integration/components/time-shortcut-picker-test.js b/app/assets/javascripts/discourse/tests/integration/components/time-shortcut-picker-test.js index d1ecae1de2..cc53e4dec8 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/time-shortcut-picker-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/time-shortcut-picker-test.js @@ -29,7 +29,7 @@ module("Integration | Component | time-shortcut-picker", function (hooks) { test("shows default options", async function (assert) { this.siteSettings.suggest_weekends_in_date_pickers = true; const tuesday = "2100-06-08T08:00:00"; - this.clock = fakeTime(tuesday, this.currentUser.timezone, true); + this.clock = fakeTime(tuesday, this.currentUser.user_option.timezone, true); await render(hbs``); @@ -55,7 +55,7 @@ module("Integration | Component | time-shortcut-picker", function (hooks) { test("show 'Later This Week' if today is < Thursday", async function (assert) { const monday = "2100-06-07T08:00:00"; - this.clock = fakeTime(monday, this.currentUser.timezone, true); + this.clock = fakeTime(monday, this.currentUser.user_option.timezone, true); await render(hbs``); @@ -64,7 +64,11 @@ module("Integration | Component | time-shortcut-picker", function (hooks) { test("does not show 'Later This Week' if today is >= Thursday", async function (assert) { const thursday = "2100-06-10T08:00:00"; - this.clock = fakeTime(thursday, this.currentUser.timezone, true); + this.clock = fakeTime( + thursday, + this.currentUser.user_option.timezone, + true + ); await render(hbs``); @@ -77,7 +81,7 @@ module("Integration | Component | time-shortcut-picker", function (hooks) { test("does not show 'Later Today' if 'Later Today' is tomorrow", async function (assert) { this.clock = fakeTime( "2100-12-11T22:00:00", // + 3 hours is tomorrow - this.currentUser.timezone, + this.currentUser.user_option.timezone, true ); @@ -92,7 +96,7 @@ module("Integration | Component | time-shortcut-picker", function (hooks) { test("shows 'Later Today' if it is before 5pm", async function (assert) { this.clock = fakeTime( "2100-12-11T16:50:00", - this.currentUser.timezone, + this.currentUser.user_option.timezone, true ); @@ -104,7 +108,7 @@ module("Integration | Component | time-shortcut-picker", function (hooks) { test("does not show 'Later Today' if it is after 5pm", async function (assert) { this.clock = fakeTime( "2100-12-11T17:00:00", - this.currentUser.timezone, + this.currentUser.user_option.timezone, true ); @@ -119,7 +123,7 @@ module("Integration | Component | time-shortcut-picker", function (hooks) { test("default custom date time is in one hour from now", async function (assert) { this.clock = fakeTime( "2100-12-11T17:00:00", - this.currentUser.timezone, + this.currentUser.user_option.timezone, true ); @@ -132,7 +136,7 @@ module("Integration | Component | time-shortcut-picker", function (hooks) { test("shows 'Next Monday' instead of 'Monday' on Sundays", async function (assert) { const sunday = "2100-01-24T08:00:00"; - this.clock = fakeTime(sunday, this.currentUser.timezone, true); + this.clock = fakeTime(sunday, this.currentUser.user_option.timezone, true); await render(hbs``); @@ -150,7 +154,7 @@ module("Integration | Component | time-shortcut-picker", function (hooks) { test("shows 'Next Monday' instead of 'Monday' on Mondays", async function (assert) { const monday = "2100-01-25T08:00:00"; - this.clock = fakeTime(monday, this.currentUser.timezone, true); + this.clock = fakeTime(monday, this.currentUser.user_option.timezone, true); await render(hbs``); @@ -169,7 +173,7 @@ module("Integration | Component | time-shortcut-picker", function (hooks) { test("the 'Next Month' option points to the first day of the next month", async function (assert) { this.clock = fakeTime( "2100-01-01T08:00:00", - this.currentUser.timezone, + this.currentUser.user_option.timezone, true ); diff --git a/app/assets/javascripts/discourse/tests/integration/components/user-menu/menu-test.js b/app/assets/javascripts/discourse/tests/integration/components/user-menu/menu-test.js index 14dbaaba8d..562784895f 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/user-menu/menu-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/user-menu/menu-test.js @@ -64,7 +64,7 @@ module("Integration | Component | user-menu", function (hooks) { }); test("likes tab is hidden if current user's like notifications frequency is 'never'", async function (assert) { - this.currentUser.set("likes_notifications_disabled", true); + this.currentUser.set("user_option.likes_notifications_disabled", true); this.currentUser.set("can_send_private_messages", true); await render(template); assert.ok(!exists("#user-menu-button-likes")); diff --git a/app/assets/javascripts/discourse/tests/integration/components/user-status-message-test.js b/app/assets/javascripts/discourse/tests/integration/components/user-status-message-test.js index aa87d17364..15e174b83a 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/user-status-message-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/user-status-message-test.js @@ -12,7 +12,7 @@ module("Integration | Component | user-status-message", function (hooks) { setupRenderingTest(hooks); hooks.beforeEach(function () { - this.currentUser.timezone = "UTC"; + this.currentUser.user_option.timezone = "UTC"; }); hooks.afterEach(function () { @@ -49,7 +49,7 @@ module("Integration | Component | user-status-message", function (hooks) { test("it shows the until TIME on the tooltip if status will expire today", async function (assert) { this.clock = fakeTime( "2100-02-01T08:00:00.000Z", - this.currentUser.timezone, + this.currentUser.user_option.timezone, true ); this.set("status", { @@ -72,7 +72,7 @@ module("Integration | Component | user-status-message", function (hooks) { test("it shows the until DATE on the tooltip if status will expire tomorrow", async function (assert) { this.clock = fakeTime( "2100-02-01T08:00:00.000Z", - this.currentUser.timezone, + this.currentUser.user_option.timezone, true ); this.set("status", { @@ -95,7 +95,7 @@ module("Integration | Component | user-status-message", function (hooks) { test("it doesn't show until datetime on the tooltip if status doesn't have expiration date", async function (assert) { this.clock = fakeTime( "2100-02-01T08:00:00.000Z", - this.currentUser.timezone, + this.currentUser.user_option.timezone, true ); this.set("status", { @@ -141,4 +141,17 @@ module("Integration | Component | user-status-message", function (hooks) { document.querySelector("[data-tippy-root] .user-status-message-tooltip") ); }); + + test("doesn't blow up with an anonymous user", async function (assert) { + this.owner.unregister("service:current-user"); + this.set("status", { + emoji: "tooth", + description: "off to dentist", + ends_at: "2100-02-02T12:30:00.000Z", + }); + + await render(hbs``); + + assert.dom(".user-status-message").exists(); + }); }); diff --git a/app/assets/javascripts/discourse/tests/integration/components/widgets/post-small-action-test.js b/app/assets/javascripts/discourse/tests/integration/components/widgets/post-small-action-test.js index 7832356449..90a7658f13 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/widgets/post-small-action-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/widgets/post-small-action-test.js @@ -16,9 +16,9 @@ module( hbs`` ); - assert.ok(!exists(".small-action-desc > .small-action-delete")); - assert.ok(!exists(".small-action-desc > .small-action-recover")); - assert.ok(!exists(".small-action-desc > .small-action-edit")); + assert.ok(!exists(".small-action-desc .small-action-delete")); + assert.ok(!exists(".small-action-desc .small-action-recover")); + assert.ok(!exists(".small-action-desc .small-action-edit")); }); test("shows edit button if canEdit", async function (assert) { @@ -29,7 +29,7 @@ module( ); assert.ok( - exists(".small-action-desc > .small-action-edit"), + exists(".small-action-desc .small-action-edit"), "it adds the edit small action button" ); }); @@ -55,11 +55,11 @@ module( ); assert.ok( - !exists(".small-action-desc > .small-action-edit"), + !exists(".small-action-desc .small-action-edit"), "it does not add the edit small action button" ); assert.ok( - exists(".small-action-desc > .small-action-recover"), + exists(".small-action-desc .small-action-recover"), "it adds the recover small action button" ); }); @@ -72,7 +72,7 @@ module( ); assert.ok( - exists(".small-action-desc > .small-action-delete"), + exists(".small-action-desc .small-action-delete"), "it adds the delete small action button" ); }); @@ -85,7 +85,7 @@ module( ); assert.ok( - exists(".small-action-desc > .small-action-recover"), + exists(".small-action-desc .small-action-recover"), "it adds the recover small action button" ); }); diff --git a/app/assets/javascripts/discourse/tests/setup-tests.js b/app/assets/javascripts/discourse/tests/setup-tests.js index 41cab84dff..570f1f00e2 100644 --- a/app/assets/javascripts/discourse/tests/setup-tests.js +++ b/app/assets/javascripts/discourse/tests/setup-tests.js @@ -28,10 +28,8 @@ import QUnit from "qunit"; import { ScrollingDOMMethods } from "discourse/mixins/scrolling"; import Session from "discourse/models/session"; import User from "discourse/models/user"; -import Site from "discourse/models/site"; import bootbox from "bootbox"; import { buildResolver } from "discourse-common/resolver"; -import { createHelperContext } from "discourse-common/lib/helpers"; import deprecated from "discourse-common/lib/deprecated"; import { flushMap } from "discourse/services/store"; import sinon from "sinon"; @@ -305,17 +303,6 @@ export default function setupTests(config) { } User.resetCurrent(); - createHelperContext({ - get siteSettings() { - return app.__container__.lookup("service:site-settings"); - }, - capabilities: {}, - get site() { - return app.__container__.lookup("service:site") || Site.current(); - }, - registry: app.__registry__, - }); - PreloadStore.reset(); resetSite(); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/category-badge-test.js b/app/assets/javascripts/discourse/tests/unit/lib/category-badge-test.js index dfde32f150..f21fcad5de 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/category-badge-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/category-badge-test.js @@ -1,9 +1,8 @@ import { module, test } from "qunit"; -import Site from "discourse/models/site"; import { categoryBadgeHTML } from "discourse/helpers/category-link"; -import sinon from "sinon"; import { getOwner } from "discourse-common/lib/get-owner"; import { setupTest } from "ember-qunit"; +import { helperContext } from "discourse-common/lib/helpers"; module("Unit | Utility | category-badge", function (hooks) { setupTest(hooks); @@ -76,10 +75,8 @@ module("Unit | Utility | category-badge", function (hooks) { id: 345, }); - sinon - .stub(Site, "currentProp") - .withArgs("uncategorized_category_id") - .returns(345); + const { site } = helperContext(); + site.set("uncategorized_category_id", 345); assert.blank( categoryBadgeHTML(uncategorized), diff --git a/app/assets/javascripts/discourse/tests/unit/lib/click-track-test.js b/app/assets/javascripts/discourse/tests/unit/lib/click-track-test.js index 67e8ea605b..669568a93b 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/click-track-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/click-track-test.js @@ -137,7 +137,7 @@ module("Unit | Utility | click-track", function (hooks) { skip("tracks external URLs when opening in another window", async function (assert) { assert.expect(3); - User.currentProp("external_links_in_new_tab", true); + User.currentProp("user_option.external_links_in_new_tab", true); const done = assert.async(); pretender.post("/clicks/track", (request) => { @@ -171,7 +171,7 @@ module("Unit | Utility | click-track", function (hooks) { }); test("does not track clicks links in quotes", async function (assert) { - User.currentProp("external_links_in_new_tab", true); + User.currentProp("user_option.external_links_in_new_tab", true); assert.notOk(track(generateClickEventOn(".quote a:last-child"))); assert.ok(window.open.calledWith("https://google.com/", "_blank")); }); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/emoji-test.js b/app/assets/javascripts/discourse/tests/unit/lib/emoji-test.js index f6f3408e1c..b5065966e0 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/emoji-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/emoji-test.js @@ -1,22 +1,27 @@ -import { discourseModule } from "discourse/tests/helpers/qunit-helpers"; import { emojiSearch } from "pretty-text/emoji"; import { emojiUnescape } from "discourse/lib/text"; -import { test } from "qunit"; +import { module, test } from "qunit"; import { IMAGE_VERSION as v } from "pretty-text/emoji/version"; +import { setupTest } from "ember-qunit"; +import { getOwner } from "discourse-common/lib/get-owner"; + +module("Unit | Utility | emoji", function (hooks) { + setupTest(hooks); -discourseModule("Unit | Utility | emoji", function () { test("emojiUnescape", function (assert) { + const siteSettings = getOwner(this).lookup("service:site-settings"); + const testUnescape = (input, expected, description, settings = {}) => { const originalSettings = {}; for (const [key, value] of Object.entries(settings)) { - originalSettings[key] = this.siteSettings[key]; - this.siteSettings[key] = value; + originalSettings[key] = siteSettings[key]; + siteSettings[key] = value; } assert.strictEqual(emojiUnescape(input), expected, description); for (const [key, value] of Object.entries(originalSettings)) { - this.siteSettings[key] = value; + siteSettings[key] = value; } }; diff --git a/app/assets/javascripts/discourse/tests/unit/lib/formatter-test.js b/app/assets/javascripts/discourse/tests/unit/lib/formatter-test.js index 415f5dade8..1b632c30f0 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/formatter-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/formatter-test.js @@ -8,15 +8,14 @@ import { until, updateRelativeAge, } from "discourse/lib/formatter"; -import { - discourseModule, - fakeTime, -} from "discourse/tests/helpers/qunit-helpers"; -import { test } from "qunit"; +import { fakeTime } from "discourse/tests/helpers/qunit-helpers"; +import { module, test } from "qunit"; import domFromString from "discourse-common/lib/dom-from-string"; +import { setupTest } from "ember-qunit"; +import { getOwner } from "discourse-common/lib/get-owner"; function formatMins(mins, opts = {}) { - let dt = new Date(new Date() - mins * 60 * 1000); + const dt = new Date(new Date() - mins * 60 * 1000); return relativeAge(dt, { format: opts.format || "tiny", leaveAgo: opts.leaveAgo, @@ -45,7 +44,9 @@ function strip(html) { return domFromString(html)[0].innerText; } -discourseModule("Unit | Utility | formatter", function (hooks) { +module("Unit | Utility | formatter", function (hooks) { + setupTest(hooks); + hooks.beforeEach(function () { this.clock = fakeTime("2012-12-31 12:00"); }); @@ -55,7 +56,7 @@ discourseModule("Unit | Utility | formatter", function (hooks) { }); test("formatting medium length dates", function (assert) { - let shortDateYear = shortDateTester("MMM D, 'YY"); + const shortDateYear = shortDateTester("MMM D, 'YY"); assert.strictEqual( strip(formatMins(1.4, { format: "medium", leaveAgo: true })), @@ -149,8 +150,10 @@ discourseModule("Unit | Utility | formatter", function (hooks) { }); test("formatting tiny dates", function (assert) { - let shortDateYear = shortDateTester("MMM 'YY"); - this.siteSettings.relative_date_duration = 14; + const siteSettings = getOwner(this).lookup("service:site-settings"); + + const shortDateYear = shortDateTester("MMM 'YY"); + siteSettings.relative_date_duration = 14; assert.strictEqual(formatMins(0), "1m"); assert.strictEqual(formatMins(1), "1m"); @@ -185,16 +188,16 @@ discourseModule("Unit | Utility | formatter", function (hooks) { assert.strictEqual(formatDays(-500), shortDateYear(-500)); assert.strictEqual(formatDays(-365 * 2 - 1), shortDateYear(-365 * 2 - 1)); // one leap year - let originalValue = this.siteSettings.relative_date_duration; - this.siteSettings.relative_date_duration = 7; + const originalValue = siteSettings.relative_date_duration; + siteSettings.relative_date_duration = 7; assert.strictEqual(formatDays(7), "7d"); assert.strictEqual(formatDays(8), shortDate(8)); - this.siteSettings.relative_date_duration = 1; + siteSettings.relative_date_duration = 1; assert.strictEqual(formatDays(1), "1d"); assert.strictEqual(formatDays(2), shortDate(2)); - this.siteSettings.relative_date_duration = 0; + siteSettings.relative_date_duration = 0; assert.strictEqual(formatMins(0), "1m"); assert.strictEqual(formatMins(1), "1m"); assert.strictEqual(formatMins(2), "2m"); @@ -203,12 +206,12 @@ discourseModule("Unit | Utility | formatter", function (hooks) { assert.strictEqual(formatDays(2), shortDate(2)); assert.strictEqual(formatDays(366), shortDateYear(366)); - this.siteSettings.relative_date_duration = null; + siteSettings.relative_date_duration = null; assert.strictEqual(formatDays(1), "1d"); assert.strictEqual(formatDays(14), "14d"); assert.strictEqual(formatDays(15), shortDate(15)); - this.siteSettings.relative_date_duration = 14; + siteSettings.relative_date_duration = 14; this.clock.restore(); this.clock = fakeTime("2012-01-12 12:00"); @@ -225,11 +228,11 @@ discourseModule("Unit | Utility | formatter", function (hooks) { assert.strictEqual(formatDays(15), shortDate(15)); assert.strictEqual(formatDays(20), shortDateYear(20)); - this.siteSettings.relative_date_duration = originalValue; + siteSettings.relative_date_duration = originalValue; }); test("autoUpdatingRelativeAge", function (assert) { - let d = moment().subtract(1, "day").toDate(); + const d = moment().subtract(1, "day").toDate(); let elem = domFromString(autoUpdatingRelativeAge(d))[0]; assert.strictEqual(elem.dataset.format, "tiny"); @@ -475,11 +478,11 @@ discourseModule("Unit | Utility | formatter", function (hooks) { }); }); -discourseModule("Unit | Utility | formatter | until", function (hooks) { +module("Unit | Utility | formatter | until", function (hooks) { + setupTest(hooks); + hooks.afterEach(function () { - if (this.clock) { - this.clock.restore(); - } + this.clock?.restore(); }); test("shows time if until moment is today", function (assert) { diff --git a/app/assets/javascripts/discourse/tests/unit/lib/link-mentions-test.js b/app/assets/javascripts/discourse/tests/unit/lib/link-mentions-test.js index 4eeb1f8d07..8aaf3779d7 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/link-mentions-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/link-mentions-test.js @@ -8,27 +8,22 @@ import domFromString from "discourse-common/lib/dom-from-string"; module("Unit | Utility | link-mentions", function () { test("linkSeenMentions replaces users and groups", async function (assert) { - pretender.get("/u/is_local_username", () => + pretender.get("/composer/mentions", () => response({ - valid: ["valid_user"], - valid_groups: ["valid_group"], - mentionable_groups: [ - { - name: "mentionable_group", - user_count: 1, - }, - ], - cannot_see: [], + users: ["valid_user"], + user_reasons: {}, + groups: { + valid_group: { user_count: 1 }, + mentionable_group: { user_count: 1 }, + }, + group_reasons: { valid_group: "not_mentionable" }, max_users_notified_per_group_mention: 100, }) ); - await fetchUnseenMentions([ - "valid_user", - "mentionable_group", - "valid_group", - "invalid", - ]); + await fetchUnseenMentions({ + names: ["valid_user", "mentionable_group", "valid_group", "invalid"], + }); const root = domFromString(`
    @@ -43,7 +38,7 @@ module("Unit | Utility | link-mentions", function () { assert.strictEqual(root.querySelector("a").innerText, "@valid_user"); assert.strictEqual(root.querySelectorAll("a")[1].innerText, "@valid_group"); assert.strictEqual( - root.querySelector("a.notify").innerText, + root.querySelector("a[data-mentionable-user-count]").innerText, "@mentionable_group" ); assert.strictEqual( diff --git a/app/assets/javascripts/discourse/tests/unit/lib/url-test.js b/app/assets/javascripts/discourse/tests/unit/lib/url-test.js index bdb99b3ce4..1d1cc04dc8 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/url-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/url-test.js @@ -154,10 +154,15 @@ module("Unit | Utility | url", function () { test("anchor handling", async function (assert) { sinon.stub(DiscourseURL, "jumpToElement"); + sinon.stub(DiscourseURL, "replaceState"); DiscourseURL.routeTo("#heading1"); assert.ok( DiscourseURL.jumpToElement.calledWith("heading1"), "in-page anchors call jumpToElement" ); + assert.ok( + DiscourseURL.replaceState.calledWith("#heading1"), + "in-page anchors call replaceState with the url fragment" + ); }); }); diff --git a/app/assets/javascripts/discourse/tests/unit/models/user-test.js b/app/assets/javascripts/discourse/tests/unit/models/user-test.js index a0a25c2260..51b51d5c20 100644 --- a/app/assets/javascripts/discourse/tests/unit/models/user-test.js +++ b/app/assets/javascripts/discourse/tests/unit/models/user-test.js @@ -94,14 +94,14 @@ module("Unit | Model | user", function (hooks) { test("createCurrent() guesses timezone if user doesn't have it set", async function (assert) { PreloadStore.store("currentUser", { username: "eviltrout", - timezone: null, + user_option: { timezone: null }, }); const expectedTimezone = "Africa/Casablanca"; sinon.stub(moment.tz, "guess").returns(expectedTimezone); const currentUser = User.createCurrent(); - assert.deepEqual(currentUser.timezone, expectedTimezone); + assert.deepEqual(currentUser.user_option.timezone, expectedTimezone); await settled(); // `User` sends a request to save the timezone }); @@ -110,7 +110,7 @@ module("Unit | Model | user", function (hooks) { const timezone = "Africa/Casablanca"; PreloadStore.store("currentUser", { username: "eviltrout", - timezone, + user_option: { timezone }, }); const spyMomentGuess = sinon.spy(moment.tz, "guess"); diff --git a/app/assets/javascripts/discourse/tests/unit/services/document-title-test.js b/app/assets/javascripts/discourse/tests/unit/services/document-title-test.js index 5fc5919f6b..d058c42569 100644 --- a/app/assets/javascripts/discourse/tests/unit/services/document-title-test.js +++ b/app/assets/javascripts/discourse/tests/unit/services/document-title-test.js @@ -34,7 +34,7 @@ module("Unit | Service | document-title", function (hooks) { test("it displays notification counts for logged in users", function (assert) { this.documentTitle.currentUser = currentUser(); - this.documentTitle.currentUser.dynamic_favicon = false; + this.documentTitle.currentUser.user_option.dynamic_favicon = false; this.documentTitle.setTitle("test notifications"); this.documentTitle.updateNotificationCount(5); assert.strictEqual(document.title, "test notifications"); @@ -52,7 +52,7 @@ module("Unit | Service | document-title", function (hooks) { date.setHours(date.getHours() + 1); this.documentTitle.currentUser.do_not_disturb_until = date.toUTCString(); - this.documentTitle.currentUser.dynamic_favicon = false; + this.documentTitle.currentUser.user_option.dynamic_favicon = false; this.documentTitle.setTitle("test notifications"); this.documentTitle.updateNotificationCount(5); assert.strictEqual(document.title, "test notifications"); diff --git a/app/assets/javascripts/pretty-text/addon/mentions.js b/app/assets/javascripts/pretty-text/addon/mentions.js new file mode 100644 index 0000000000..a81fb2c501 --- /dev/null +++ b/app/assets/javascripts/pretty-text/addon/mentions.js @@ -0,0 +1,31 @@ +export function mentionRegex(unicodeUsernames) { + if (unicodeUsernames) { + try { + // Create the regex from a string, because Babel doesn't understand + // Unicode property escapes and completely mangles the regexp. + const alnum = "\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}"; + return new RegExp( + `@([${alnum}_][${alnum}._-]{0,58}[${alnum}])|@([${alnum}_])`, + "u" + ); + } catch (e) { + if (!(e instanceof SyntaxError)) { + throw e; + } + + // Fallback for older browsers and MiniRacer. + // Created with regexpu-core@4.5.4 by executing the following in nodejs: + // + // const rewritePattern = require('regexpu-core') + // new RegExp(rewritePattern(/[\p{Alphabetic}\p{Mark}\p{Decimal_Number}]/u.source, 'u', { 'unicodePropertyEscape': true })) + const alnum = + /(?:[0-9A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u052F\u0531-\u0556\u0559\u0560-\u0588\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05EF-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06E1-\u06E8\u06DF-\u06E4\u06ED-\u06F9\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u07FD\u0800-\u082D\u0840-\u085B\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u08D3-\u08E1\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u09FC\u09FE\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9-\u0AFF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C80-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D00-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D54-\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1878\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABE\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CD0-\u1CD2\u1CD4-\u1CFA\u1D00-\u1DF9\u1DFB-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u20D0-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u24B6-\u24E9\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEF\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA672\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7BF\uA7C2-\uA7C6\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C5\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB67\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD27\uDD30-\uDD39\uDF00-\uDF1C\uDF27\uDF30-\uDF50\uDFE0-\uDFF6]|\uD804[\uDC00-\uDC46\uDC66-\uDC6F\uDC82-\uDCBA\uDC7F-\uDC82\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD44-\uDD46\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDC9-\uDDCC\uDDD0-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE3E\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3B-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC00-\uDC4A\uDC50-\uDC59\uDC5E\uDC5F\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB8\uDEC0-\uDEC9\uDF00-\uDF1A\uDF1D-\uDF2B\uDF30-\uDF39]|\uD806[\uDC00-\uDC3A\uDCA0-\uDCE9\uDCFF\uDDA0-\uDDA7\uDDAA-\uDDD7\uDDDA-\uDDE1\uDDE3\uDDE4\uDE00-\uDE3E\uDE47\uDE50-\uDE99\uDE9D\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC36\uDC38-\uDC40\uDC50-\uDC59\uDC72-\uDC8F\uDC92-\uDCA7\uDCA9-\uDCB6\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD36\uDD3A\uDD3C\uDD3D\uDD3F-\uDD47\uDD50-\uDD59\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD8E\uDD90\uDD91\uDD93-\uDD98\uDDA0-\uDDA9\uDEE0-\uDEF6]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF4A\uDF4F-\uDF87\uDF8F-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD50-\uDD52\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A\uDD00-\uDD2C\uDD30-\uDD3D\uDD40-\uDD49\uDD4E\uDEC0-\uDEF9]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6\uDD00-\uDD4B\uDD50-\uDD59]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD83C[\uDD30-\uDD49\uDD50-\uDD69\uDD70-\uDD89]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uDB40[\uDD00-\uDDEF])/ + .source; + return new RegExp( + `@((?:_|${alnum})(?:[._-]|${alnum}){0,58}${alnum})|@(?:(_|${alnum}))` + ); + } + } else { + return /@(\w[\w.-]{0,58}[^\W_])|@(\w)/; + } +} diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/hashtag-autocomplete.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/hashtag-autocomplete.js index b920b72b1a..65e2fca64b 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/hashtag-autocomplete.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/hashtag-autocomplete.js @@ -14,11 +14,7 @@ function addHashtag(buffer, matches, state) { // slug lookup. const result = hashtagLookup && - hashtagLookup( - slug, - options.currentUser, - options.hashtagTypesInPriorityOrder - ); + hashtagLookup(slug, options.userId, options.hashtagTypesInPriorityOrder); // NOTE: When changing the HTML structure here, you must also change // it in the placeholder HTML code inside lib/hashtag-autocomplete, and vice-versa. diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js index 02c48ebeca..2342c19dda 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js @@ -1,3 +1,5 @@ +import { mentionRegex } from "pretty-text/mentions"; + function addMention(buffer, matches, state) { let username = matches[1] || matches[2]; let tag = "span"; @@ -32,35 +34,3 @@ export function setup(helper) { md.core.textPostProcess.ruler.push("mentions", rule); }); } - -export function mentionRegex(unicodeUsernames) { - if (unicodeUsernames) { - try { - // Create the regex from a string, because Babel doesn't understand - // Unicode property escapes and completely mangles the regexp. - const alnum = "\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}"; - return new RegExp( - `@([${alnum}_][${alnum}._-]{0,58}[${alnum}])|@([${alnum}_])`, - "u" - ); - } catch (e) { - if (!(e instanceof SyntaxError)) { - throw e; - } - - // Fallback for older browsers and MiniRacer. - // Created with regexpu-core@4.5.4 by executing the following in nodejs: - // - // const rewritePattern = require('regexpu-core') - // new RegExp(rewritePattern(/[\p{Alphabetic}\p{Mark}\p{Decimal_Number}]/u.source, 'u', { 'unicodePropertyEscape': true })) - const alnum = - /(?:[0-9A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u052F\u0531-\u0556\u0559\u0560-\u0588\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05EF-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06E1-\u06E8\u06DF-\u06E4\u06ED-\u06F9\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u07FD\u0800-\u082D\u0840-\u085B\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u08D3-\u08E1\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u09FC\u09FE\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9-\u0AFF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C80-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D00-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D54-\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1878\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABE\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CD0-\u1CD2\u1CD4-\u1CFA\u1D00-\u1DF9\u1DFB-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u20D0-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u24B6-\u24E9\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEF\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA672\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7BF\uA7C2-\uA7C6\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C5\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB67\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD27\uDD30-\uDD39\uDF00-\uDF1C\uDF27\uDF30-\uDF50\uDFE0-\uDFF6]|\uD804[\uDC00-\uDC46\uDC66-\uDC6F\uDC82-\uDCBA\uDC7F-\uDC82\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD44-\uDD46\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDC9-\uDDCC\uDDD0-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE3E\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3B-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC00-\uDC4A\uDC50-\uDC59\uDC5E\uDC5F\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB8\uDEC0-\uDEC9\uDF00-\uDF1A\uDF1D-\uDF2B\uDF30-\uDF39]|\uD806[\uDC00-\uDC3A\uDCA0-\uDCE9\uDCFF\uDDA0-\uDDA7\uDDAA-\uDDD7\uDDDA-\uDDE1\uDDE3\uDDE4\uDE00-\uDE3E\uDE47\uDE50-\uDE99\uDE9D\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC36\uDC38-\uDC40\uDC50-\uDC59\uDC72-\uDC8F\uDC92-\uDCA7\uDCA9-\uDCB6\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD36\uDD3A\uDD3C\uDD3D\uDD3F-\uDD47\uDD50-\uDD59\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD8E\uDD90\uDD91\uDD93-\uDD98\uDDA0-\uDDA9\uDEE0-\uDEF6]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF4A\uDF4F-\uDF87\uDF8F-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD50-\uDD52\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A\uDD00-\uDD2C\uDD30-\uDD3D\uDD40-\uDD49\uDD4E\uDEC0-\uDEF9]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6\uDD00-\uDD4B\uDD50-\uDD59]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD83C[\uDD30-\uDD49\uDD50-\uDD69\uDD70-\uDD89]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uDB40[\uDD00-\uDDEF])/ - .source; - return new RegExp( - `@((?:_|${alnum})(?:[._-]|${alnum}){0,58}${alnum})|@(?:(_|${alnum}))` - ); - } - } else { - return /@(\w[\w.-]{0,58}[^\W_])|@(\w)/; - } -} diff --git a/app/assets/javascripts/select-kit/addon/components/future-date-input-selector.js b/app/assets/javascripts/select-kit/addon/components/future-date-input-selector.js index e294171f65..4141d8393c 100644 --- a/app/assets/javascripts/select-kit/addon/components/future-date-input-selector.js +++ b/app/assets/javascripts/select-kit/addon/components/future-date-input-selector.js @@ -18,7 +18,7 @@ export default ComboBoxComponent.extend({ init() { this._super(...arguments); - this.userTimezone = this.currentUser.timezone; + this.userTimezone = this.currentUser.user_option.timezone; }, modifyComponentForRow() { diff --git a/app/assets/javascripts/select-kit/addon/components/topic-notifications-button.js b/app/assets/javascripts/select-kit/addon/components/topic-notifications-button.js index d56dd51a69..8927c09614 100644 --- a/app/assets/javascripts/select-kit/addon/components/topic-notifications-button.js +++ b/app/assets/javascripts/select-kit/addon/components/topic-notifications-button.js @@ -61,7 +61,7 @@ export default Component.extend({ if ( this.currentUser && - this.currentUser.mailing_list_mode && + this.currentUser.user_option.mailing_list_mode && level > NotificationLevels.MUTED ) { return I18n.t("topic.notifications.reasons.mailing_list_mode"); diff --git a/app/assets/javascripts/yarn.lock b/app/assets/javascripts/yarn.lock index ea4793714b..e45003dd98 100644 --- a/app/assets/javascripts/yarn.lock +++ b/app/assets/javascripts/yarn.lock @@ -1086,13 +1086,14 @@ ember-cli-version-checker "^5.1.2" semver "^7.3.5" -"@embroider/addon-shim@^1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@embroider/addon-shim/-/addon-shim-1.8.3.tgz#2368510b8ce42d50d02cb3289c32e260dfa34bd9" - integrity sha512-7pyHwzT6ESXc3nZsB8rfnirLkUhQWdvj6CkYH+0MUPN74mX4rslf7pnBqZE/KZkW3uBIaBYvU8fxi0hcKC/Paw== +"@embroider/addon-shim@^1.8.3", "@embroider/addon-shim@^1.8.4": + version "1.8.4" + resolved "https://registry.yarnpkg.com/@embroider/addon-shim/-/addon-shim-1.8.4.tgz#0e7f32c5506bf0f3eb0840506e31c36c7053763c" + integrity sha512-sFhfWC0vI18KxVenmswQ/ShIvBg4juL8ubI+Q3NTSdkCTeaPQ/DIOUF6oR5DCQ8eO/TkIaw+kdG3FkTY6yNJqA== dependencies: - "@embroider/shared-internals" "^1.8.3" - semver "^7.3.5" + "@embroider/shared-internals" "^2.0.0" + broccoli-funnel "^3.0.8" + semver "^7.3.8" "@embroider/macros@1.8.3": version "1.8.3" @@ -1122,7 +1123,7 @@ resolve "^1.20.0" semver "^7.3.2" -"@embroider/shared-internals@1.8.3", "@embroider/shared-internals@^1.8.3": +"@embroider/shared-internals@1.8.3": version "1.8.3" resolved "https://registry.yarnpkg.com/@embroider/shared-internals/-/shared-internals-1.8.3.tgz#52d868dc80016e9fe983552c0e516f437bf9b9f9" integrity sha512-N5Gho6Qk8z5u+mxLCcMYAoQMbN4MmH+z2jXwQHVs859bxuZTxwF6kKtsybDAASCtd2YGxEmzcc1Ja/wM28824w== @@ -1418,10 +1419,10 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== -"@transloadit/prettier-bytes@0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz#cdb5399f445fdd606ed833872fa0cabdbc51686b" - integrity sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA== +"@transloadit/prettier-bytes@0.0.9": + version "0.0.9" + resolved "https://registry.yarnpkg.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.9.tgz#8d3146f75fd9d3c544cb63ec7dbdeb6670d3e2d7" + integrity sha512-pCvdmea/F3Tn4hAtHqNXmjcixSaroJJ+L3STXlYJdir1g1m2mRQpWbN8a4SvgQtaw2930Ckhdx8qXdXBFMKbAA== "@types/body-parser@*": version "1.19.0" @@ -1573,73 +1574,73 @@ resolved "https://registry.yarnpkg.com/@types/symlink-or-copy/-/symlink-or-copy-1.2.0.tgz#4151a81b4052c80bc2becbae09f3a9ec010a9c7a" integrity sha512-Lja2xYuuf2B3knEsga8ShbOdsfNOtzT73GyJmZyY7eGl2+ajOqrs8yM5ze0fsSoYwvA6bw7/Qr7OZ7PEEmYwWg== -"@uppy/aws-s3-multipart@^2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@uppy/aws-s3-multipart/-/aws-s3-multipart-2.4.1.tgz#b7e5192ed94b3b56d3d9917d2abc1393b2d2af30" - integrity sha512-7p+4NhSRrFuGpmaq+fGXxkOEx85+x017TIQPEaN86wvSAaNW9SywEBDvg1QUYE/v/SZQ/rAe3JOMZ2YDTPrWaA== +"@uppy/aws-s3-multipart@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@uppy/aws-s3-multipart/-/aws-s3-multipart-3.1.1.tgz#996184d7c73122d9a33b166e2960215e482e57ab" + integrity sha512-vL4QtbI5T4cGJLHm2GzKIGeaiy2zbgNW0nn/3LxYQ+wrBHLJoVZ/6uzMiFdNUBNa28rRQ/qxWTmy/kD6co95cg== dependencies: - "@uppy/companion-client" "^2.2.1" - "@uppy/utils" "^4.1.0" + "@uppy/companion-client" "^3.1.1" + "@uppy/utils" "^5.1.1" -"@uppy/aws-s3@^2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@uppy/aws-s3/-/aws-s3-2.2.1.tgz#b097103bb3cdc96f9b3fc97089f12254a7dce2fb" - integrity sha512-nGRdXWOFNpYU31bD1gcLK1i8ClY7tRvvsSR70bzYI5gFC2Vw07sHk6Tc3yKi9doQoWJ3OnE3IilvfK3iZ/NRmg== +"@uppy/aws-s3@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@uppy/aws-s3/-/aws-s3-3.0.4.tgz#a2422a2091c57570ff99a77108296f09ba9e46a5" + integrity sha512-mcwRYUH1TgcCWAkmvx9SCu7A9ylgBX+b1qyVO7ZiHnSbdg1F0H9hd3KkiUHGHkuV+hHz+l4vM9tQJqhxVhpW8w== dependencies: - "@uppy/companion-client" "^2.2.1" - "@uppy/utils" "^4.1.0" - "@uppy/xhr-upload" "^2.1.2" - nanoid "^3.1.25" + "@uppy/companion-client" "^3.0.2" + "@uppy/utils" "^5.0.2" + "@uppy/xhr-upload" "^3.0.4" + nanoid "^4.0.0" -"@uppy/companion-client@^2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@uppy/companion-client/-/companion-client-2.2.1.tgz#cef4b15185dd8ee6024386c8aae809e8e05dcd53" - integrity sha512-Y3E10NJLMfp/wjgthNhx3gJtT67fzFCPNPFwpNNRs5iJsW6PANhJ420eyMUFzfmEZ56ZzGYxr5pzJZx8YxHICQ== +"@uppy/companion-client@^3.0.2", "@uppy/companion-client@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@uppy/companion-client/-/companion-client-3.1.1.tgz#8c9974f70b899a40771da9a90113941356f9842e" + integrity sha512-S1M37vjWu8gdOgdI3Hh/1fVZ9GaLdyPQjVyUujZDTsr79b4VG7v/zjdqJ0FiOTjfGbpnj8s9kr1uyYi0Zf5VFw== dependencies: - "@uppy/utils" "^4.1.0" + "@uppy/utils" "^5.1.1" namespace-emitter "^2.0.1" -"@uppy/core@^2.3.1": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@uppy/core/-/core-2.3.1.tgz#1d7a40f0a0b96a2709115bf7d087b4e6ab1403f4" - integrity sha512-KV04X7ueYbYX1p37/i3QsoQSw8IDP8Yb+Bh9KNN0X2Vcun6K2VnNjhVtPmPXtyjDZooK7lVIqhRX8TZWcSfgSQ== +"@uppy/core@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@uppy/core/-/core-3.0.4.tgz#3bdc08e50ab72749e6f9afa60eec6c836e8b3442" + integrity sha512-vFofKmmVVsQE9bnOXozAPy94kLQMUdMH/l8m4ncXmxyyGRc2e9VfvY9wiy2EEsoj11O7YVzHOP70FYdRReUpVw== dependencies: - "@transloadit/prettier-bytes" "0.0.7" - "@uppy/store-default" "^2.1.0" - "@uppy/utils" "^4.1.0" + "@transloadit/prettier-bytes" "0.0.9" + "@uppy/store-default" "^3.0.2" + "@uppy/utils" "^5.0.2" lodash.throttle "^4.1.1" mime-match "^1.0.2" namespace-emitter "^2.0.1" - nanoid "^3.1.25" + nanoid "^4.0.0" preact "^10.5.13" -"@uppy/drop-target@^1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@uppy/drop-target/-/drop-target-1.1.3.tgz#cb8eb4ff3c6f6e3733c57ad46f9422186c024e56" - integrity sha512-Cd+L3mdVovS6G0Yv0Hah18Utz3czQ75YrGJ65MlHFzfJccOQJczvxn18FVssKyO6Zc0ce1y0mGKGjUClTM5dMA== +"@uppy/drop-target@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@uppy/drop-target/-/drop-target-2.0.1.tgz#94e106056a846d9de12b0d12231f66c9718a6778" + integrity sha512-FMO8wj+0dx4mlwXKxFWSTUF+irgr0BVXadyc4qaoBBtZ3vEcwc3jP7SQfwk3JizV/D5MYG8MRICRbPAIrY9M8w== dependencies: - "@uppy/utils" "^4.0.7" + "@uppy/utils" "^5.0.2" -"@uppy/store-default@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@uppy/store-default/-/store-default-2.1.0.tgz#3fbcac626dd515668b88762d812017bd4f9b75d4" - integrity sha512-BkcR1wGw6Kwbvr8m1tKF9EDDWSTJoTGnVseBF/iW4bzR22assbtxZIE1iroo68UMqYEG4rv63SX4BUEtNvVjdA== +"@uppy/store-default@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@uppy/store-default/-/store-default-3.0.2.tgz#870724c45a2f671d625123cb4a412e3bfae935d9" + integrity sha512-kIQMCjXui6tjF1E9xGo4YHkvt71McXkU0FStrcQuBrRXuOhb+NcuWh3sMh3KryivVNgT6w5Odrlw2FUFkl9cqA== -"@uppy/utils@^4.0.7", "@uppy/utils@^4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@uppy/utils/-/utils-4.1.0.tgz#19f3f08cd21b383cdcdf95adbec76d9c1a694b68" - integrity sha512-C47DUl4uLzmQZdW+VmetIgGRurXuPsvb+/pyYqh9DJn0Phep8u7AOj/tlJA5CHv4pefNHsFjXpaWfSUG3HtW3A== +"@uppy/utils@^5.0.2", "@uppy/utils@^5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@uppy/utils/-/utils-5.1.1.tgz#9597e8696e17d71413672bd56eb082c7410514a3" + integrity sha512-uoI+PcIVQboky0ZbN4PQeK1seZnnJocomzeK7blId9HKJ6QNgZLf2ibk2CQuQxrOuNsWhgrhs5uLO5Si0oM0Yw== dependencies: lodash.throttle "^4.1.1" -"@uppy/xhr-upload@^2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@uppy/xhr-upload/-/xhr-upload-2.1.2.tgz#0f644e3371b611b10cd5ddbd7d799ab9f88a3786" - integrity sha512-VCsb7J5yHsof49nnUa+Y1n27UMtqHPttQmmoCa5hmjqa9R7ZISpBkXKOQmZo526eopKNuAKSAdkHWfCm8efJTA== +"@uppy/xhr-upload@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@uppy/xhr-upload/-/xhr-upload-3.0.4.tgz#219a92c832bee1f089992958d27ec71dbe9d9d7d" + integrity sha512-uJ1oxcwEaSLnrexvi6Lp57hV3z3DsovgVmYIVwg+z/EnrRcL32wNRE7FcIr8Mk9e1jdMiFYlk6cQmiP2dZep8A== dependencies: - "@uppy/companion-client" "^2.2.1" - "@uppy/utils" "^4.1.0" - nanoid "^3.1.25" + "@uppy/companion-client" "^3.0.2" + "@uppy/utils" "^5.0.2" + nanoid "^4.0.0" "@webassemblyjs/ast@1.11.1": version "1.11.1" @@ -3533,9 +3534,9 @@ decimal.js@^10.4.2: integrity sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA== decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.3" @@ -4203,6 +4204,15 @@ ember-modifier@^3.2.7: ember-cli-typescript "^5.0.0" ember-compatibility-helpers "^1.2.5" +ember-modifier@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/ember-modifier/-/ember-modifier-4.0.0.tgz#0bb3fae11435fcbe0d3dfa852ce224d81d75ddb2" + integrity sha512-OdconmrqKP2haK4kBwNmtnA2NiC2MFmIJC3LgJ1WhwZ49GaktM+bRIuFxF/S5W0oaegzKs1qH2ZDlqMeO2L3nw== + dependencies: + "@embroider/addon-shim" "^1.8.4" + ember-cli-normalize-entity-name "^1.0.0" + ember-cli-string-utils "^1.1.0" + ember-on-resize-modifier@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ember-on-resize-modifier/-/ember-on-resize-modifier-1.1.0.tgz#96b92cb190a552a8e240a2077037b0b71facc54e" @@ -4533,10 +4543,10 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@^8.28.0: - version "8.28.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.28.0.tgz#81a680732634677cc890134bcdd9fdfea8e63d6e" - integrity sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ== +eslint@^8.29.0: + version "8.29.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.29.0.tgz#d74a88a20fb44d59c51851625bc4ee8d0ec43f87" + integrity sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg== dependencies: "@eslint/eslintrc" "^1.3.3" "@humanwhocodes/config-array" "^0.11.6" @@ -7096,11 +7106,16 @@ namespace-emitter@^2.0.1: resolved "https://registry.yarnpkg.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz#978d51361c61313b4e6b8cf6f3853d08dfa2b17c" integrity sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g== -nanoid@^3.1.25, nanoid@^3.1.30: +nanoid@^3.1.30: version "3.2.0" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== +nanoid@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-4.0.0.tgz#6e144dee117609232c3f415c34b0e550e64999a5" + integrity sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -8218,10 +8233,10 @@ sane@^4.0.0, sane@^4.1.0: minimist "^1.1.1" walker "~1.0.5" -sass@^1.56.1: - version "1.56.1" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.1.tgz#94d3910cd468fd075fa87f5bb17437a0b617d8a7" - integrity sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ== +sass@^1.56.2: + version "1.56.2" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.2.tgz#9433b345ab3872996c82a53a58c014fd244fd095" + integrity sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -8284,6 +8299,13 @@ semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: dependencies: lru-cache "^6.0.0" +semver@^7.3.8: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -8857,10 +8879,10 @@ terser-webpack-plugin@^5.1.3: source-map "^0.6.1" terser "^5.7.2" -terser@^5.16.0, terser@^5.3.0, terser@^5.7.2: - version "5.16.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.0.tgz#29362c6f5506e71545c73b069ccd199bb28f7f54" - integrity sha512-KjTV81QKStSfwbNiwlBXfcgMcOloyuRdb62/iLFPGBcVNF4EXjhdYBhYHmbJpiBrVxZhDvltE11j+LBQUxEEJg== +terser@^5.16.1, terser@^5.3.0, terser@^5.7.2: + version "5.16.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.1.tgz#5af3bc3d0f24241c7fb2024199d5c461a1075880" + integrity sha512-xvQfyfA1ayT0qdK47zskQgRZeWLoOQ8JQ6mIgRGVNwZKdQMU+5FkCBjmv4QjcrTzyZquRw2FVtlJSRUmMKQslw== dependencies: "@jridgewell/source-map" "^0.3.2" acorn "^8.5.0" diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 07177c845e..4687bda189 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -1009,6 +1009,7 @@ a.inline-editable-field { @import "common/admin/dashboard"; @import "common/admin/settings"; @import "common/admin/users"; +@import "common/admin/penalty"; @import "common/admin/suspend"; @import "common/admin/badges"; @import "common/admin/emails"; diff --git a/app/assets/stylesheets/common/admin/api.scss b/app/assets/stylesheets/common/admin/api.scss index 19ca2c84ec..8ca8e2478f 100644 --- a/app/assets/stylesheets/common/admin/api.scss +++ b/app/assets/stylesheets/common/admin/api.scss @@ -260,19 +260,31 @@ table.api-keys { .instructions { margin-top: 5px; } + .subscription-choice { margin-bottom: 10px; + + label { + display: inline-block; + } } } -.web-hook-direction { - a, - button { - margin-right: 10px; - } +.admin-webhooks__summary { + margin-bottom: 1rem; +} + +.admin-webhooks__edit-button, +.admin-webhooks__delete-button { + font-size: var(--font-0-rem); } .web-hook-events { + .heading-container { + width: 100%; + background-color: var(--primary-low); + } + li { padding: 2px 0; } @@ -283,6 +295,13 @@ table.api-keys { overflow-y: auto; overflow-x: hidden; } + .col.heading { + font-weight: bold; + padding: 4px 0; + } + .col.heading.actions { + padding: 4px 0; + } .col.first { width: 90px; } @@ -296,18 +315,14 @@ table.api-keys { width: 250px; } .col.actions { - width: 455px; padding-top: 0; a { text-decoration: underline; } } - .col.heading.actions { - padding: 4px 0; - } .details { display: block; - margin-top: 10px; + margin-top: 1rem; } label { font-size: var(--font-0); @@ -317,10 +332,13 @@ table.api-keys { } } +.webhook-events__ping-button { + margin-bottom: 1rem; +} + .web-hook-events-listing { - margin-top: 15px; .alert { - margin: 15px 0 0 0; + margin: 0; } } diff --git a/app/assets/stylesheets/common/admin/penalty.scss b/app/assets/stylesheets/common/admin/penalty.scss new file mode 100644 index 0000000000..c54f63d8cf --- /dev/null +++ b/app/assets/stylesheets/common/admin/penalty.scss @@ -0,0 +1,11 @@ +.silence-user-modal, +.suspend-user-modal { + .table { + width: 100%; + + th, + td { + padding: 8px 0px; + } + } +} diff --git a/app/assets/stylesheets/common/admin/staff_logs.scss b/app/assets/stylesheets/common/admin/staff_logs.scss index 9aa7443a2f..a3252cc1d6 100644 --- a/app/assets/stylesheets/common/admin/staff_logs.scss +++ b/app/assets/stylesheets/common/admin/staff_logs.scss @@ -1,28 +1,5 @@ // Styles for /admin/logs -.web-hook-events { - border-bottom: dotted 1px var(--primary-low-mid); - .heading-container { - width: 100%; - background-color: var(--primary-low); - } - .col.heading { - font-weight: bold; - padding: 4px 0; - } - .col { - display: inline-block; - padding-top: 6px; - vertical-align: top; - overflow-y: auto; - overflow-x: hidden; - } - .ember-list-item-view { - width: 100%; - border-top: solid 1px var(--primary-low); - } -} - .log-details-modal { pre { white-space: pre-wrap; diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss index cfde43de91..2aa063297a 100644 --- a/app/assets/stylesheets/common/base/_topic-list.scss +++ b/app/assets/stylesheets/common/base/_topic-list.scss @@ -81,15 +81,6 @@ } } -.show-more { - width: 100%; - z-index: z("base"); - &.has-topics { - position: absolute; - top: 7px; - } -} - .category-heading { max-width: 100%; p { @@ -351,9 +342,11 @@ #list-area { margin-bottom: 100px; + .empty-topic-list { padding: 10px; } + .unseen { background-color: transparent; padding: 0; @@ -362,6 +355,18 @@ font-size: var(--font-0); cursor: default; } + + .show-more { + width: 100%; + z-index: z("base"); + position: absolute; + top: 0; + + .alert { + margin: 0; + padding: 1.1em 2em 1.1em 0.65em; + } + } } .topic-list { diff --git a/app/assets/stylesheets/common/base/new-user.scss b/app/assets/stylesheets/common/base/new-user.scss index 6071486dbe..b7afd61970 100644 --- a/app/assets/stylesheets/common/base/new-user.scss +++ b/app/assets/stylesheets/common/base/new-user.scss @@ -102,9 +102,23 @@ padding: 0.5em 1em; } + .user-nav-messages-dropdown { + // manage long group names + max-width: 20vw; + min-width: 7em; + .select-kit-selected-name, + .name { + @include ellipsis; + } + .name { + min-width: 0; + } + } + .category-breadcrumb { + width: auto; padding-top: var(--navigation-secondary-padding-top); - @include breakpoint(large) { + @include breakpoint(extra-large) { font-size: var(--font-down-1); } > li { @@ -115,7 +129,7 @@ .navigation-controls { padding-top: var(--navigation-secondary-padding-top); flex-wrap: nowrap; - @include breakpoint(large) { + @include breakpoint(extra-large) { font-size: var(--font-down-1); } } diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index d21e8a6f57..3f7cb8fc0d 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -1059,6 +1059,7 @@ kbd { box-sizing: border-box; color: var(--primary); display: inline-flex; + gap: 0 0.5em; // space between text and images/emoji font-size: var(--font-down-1); justify-content: center; line-height: var(--line-height-large); @@ -1123,121 +1124,107 @@ blockquote > *:last-child { display: flex; align-items: center; + &.deleted { + background-color: var(--danger-low-mid); + } + .topic-avatar, .small-action-desc { border-top: 1px solid var(--primary-low); } .topic-avatar { - align-self: flex-start; - padding: 0.7em 0; + display: flex; + align-self: stretch; flex: 0 0 auto; + margin: 0; + padding-top: 1em; width: var(--topic-avatar-width); + justify-content: center; .d-icon { - font-size: 2em; + font-size: var(--font-up-3); width: var(--topic-avatar-width); - text-align: center; color: var(--primary-low-mid); } } - &.deleted { - background-color: var(--danger-low-mid); - } - - .small-action-desc.timegap { - color: var(--primary-medium); - } - .small-action-desc { box-sizing: border-box; display: flex; flex-wrap: wrap; - align-items: center; + color: var(--primary-700); padding: 1em 0 1em var(--topic-body-width-padding); - text-transform: uppercase; - font-weight: bold; - font-size: var(--font-down-1); - color: var(--primary-medium); width: calc( var(--topic-body-width) + (var(--topic-body-width-padding) * 2) ); min-width: 0; // Allows flex container to shrink - button { - align-self: flex-start; - font-size: var(--font-up-1); - .d-icon { - font-size: var(--font-down-1); - } - } - - .custom-message { - flex: 1 1 100%; - text-transform: none; - font-weight: normal; - font-size: var(--font-up-1); - order: 12; - word-break: break-word; - min-width: 0; // Allows content like oneboxes to shrink - p { - margin-bottom: 0; - } - } - a.trigger-user-card { - align-self: stretch; - } .avatar { - margin-right: 0.8em; + margin-right: 0.5em; float: left; } - > p { + p { margin: 0; padding-right: 0.5em; - line-height: var(--line-height-medium); - flex: 1 1; - .mention { - // needs some special sizing and positioning due to surrounding all-caps text - position: relative; - top: -1px; - font-size: var(--font-0); - text-transform: initial; - } + } + } + + .small-action-contents { + flex: 1 1 auto; + } + + .small-action-buttons { + margin-left: auto; + } + + .small-action-custom-message { + flex: 1 1 100%; + font-weight: normal; + margin-top: 0.5em; + word-break: break-word; + min-width: 0; // Allows content like oneboxes to shrink + color: var(--primary); + p { + margin-bottom: 0; } } button { background: transparent; - border: 0; - order: 9; - &:last-of-type { - margin-left: auto; - order: 8; + font-size: var(--font-down-1); + .d-icon { + color: var(--primary-500); + } + + .discourse-no-touch & { + &:hover, + &:focus { + background: var(--primary-200); + .d-icon { + color: var(--primary); + } + } } } &.topic-post-visited { - border-top: none; - - + .small-action { - border-top: none; - } - .topic-post-visited-line { text-align: center; border-bottom: 1px solid var(--danger-medium); - line-height: 0.1em; - margin: 1rem 0px; + z-index: z("base") + 2; // ensures last visit border is on top of post borders + line-height: 0; + margin: 0; + margin-bottom: -1px; + color: var(--danger-medium); + font-size: var(--font-down-1); width: calc( var(--topic-body-width) + var(--topic-avatar-width) + (var(--topic-body-width-padding) * 2) ); .topic-post-visited-message { background-color: var(--secondary); - color: var(--danger-medium); - font-size: var(--font-down-1); - padding: 0 8px; + padding: 0 0.5em; } } } diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss index 2c4399189a..9f0b5fa8fb 100644 --- a/app/assets/stylesheets/common/base/user.scss +++ b/app/assets/stylesheets/common/base/user.scss @@ -758,6 +758,19 @@ } .second-factor-item { margin-top: 0.75em; + width: 500px; + display: flex; + justify-content: space-between; + border-bottom: 1px solid #ddd; + margin: 5px 0px; + align-items: center; + + &:last-child { + border-bottom: 0; + } + .btn-danger .d-icon { + color: var(--danger); + } } .btn.edit { min-height: auto; diff --git a/app/assets/stylesheets/common/components/hashtag.scss b/app/assets/stylesheets/common/components/hashtag.scss index 6bae5c75ba..38876ec39f 100644 --- a/app/assets/stylesheets/common/components/hashtag.scss +++ b/app/assets/stylesheets/common/components/hashtag.scss @@ -27,6 +27,12 @@ a.hashtag-cooked { font-size: var(--font-down-1); margin: 0 0.2em 0 0.1em; } + + img.emoji { + width: 15px; + height: 15px; + vertical-align: text-bottom; + } } .hashtag-autocomplete { diff --git a/app/assets/stylesheets/common/foundation/base.scss b/app/assets/stylesheets/common/foundation/base.scss index f233601f80..5dc173099e 100644 --- a/app/assets/stylesheets/common/foundation/base.scss +++ b/app/assets/stylesheets/common/foundation/base.scss @@ -1,3 +1,17 @@ +// These CSS custom properties are added here instead of in variables.scss +// because variables.scss is injected into every theme CSS file +// which causes problems when overriding custom properties in themes + +:root { + --topic-body-width: #{$topic-body-width}; + --topic-body-width-padding: #{$topic-body-width-padding}; + --topic-avatar-width: #{$topic-avatar-width}; + --d-border-radius: initial; + --d-nav-pill-border-radius: var(--d-border-radius); + --d-button-border-radius: var(--d-border-radius); + --d-input-border-radius: var(--d-border-radius); +} + // -------------------------------------------------- // Base styles for HTML elements // -------------------------------------------------- diff --git a/app/assets/stylesheets/common/foundation/color_transformations.scss b/app/assets/stylesheets/common/foundation/color_transformations.scss index 9b4325536c..761d5e706c 100644 --- a/app/assets/stylesheets/common/foundation/color_transformations.scss +++ b/app/assets/stylesheets/common/foundation/color_transformations.scss @@ -18,7 +18,7 @@ $primary-300: dark-light-diff($primary, $secondary, 80%, -60%) !default; $primary-400: dark-light-diff($primary, $secondary, 70%, -45%) !default; $primary-500: dark-light-diff($primary, $secondary, 60%, -40%) !default; $primary-600: dark-light-diff($primary, $secondary, 50%, -35%) !default; -$primary-700: dark-light-diff($primary, $secondary, 30%, -30%) !default; +$primary-700: dark-light-diff($primary, $secondary, 38%, -30%) !default; $primary-800: dark-light-diff($primary, $secondary, 30%, -25%) !default; $primary-900: dark-light-diff($primary, $secondary, 15%, -10%) !default; diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss index cf8a1bc0fa..2f9bafab9e 100644 --- a/app/assets/stylesheets/common/foundation/variables.scss +++ b/app/assets/stylesheets/common/foundation/variables.scss @@ -1,5 +1,6 @@ // -------------------------------------------------- // Variables used throughout the theme +// this file is injected into every theme CSS file // -------------------------------------------------- // Layout dimensions @@ -14,16 +15,6 @@ $topic-body-width-padding: 11px; $topic-avatar-width: 45px; $reply-area-max-width: 1475px !default; -:root { - --topic-body-width: #{$topic-body-width}; - --topic-body-width-padding: #{$topic-body-width-padding}; - --topic-avatar-width: #{$topic-avatar-width}; - --d-border-radius: initial; - --d-nav-pill-border-radius: var(--d-border-radius); - --d-button-border-radius: var(--d-border-radius); - --d-input-border-radius: var(--d-border-radius); -} - $d-sidebar-width: 16em !default; // Brand color variables diff --git a/app/assets/stylesheets/common/topic-timeline.scss b/app/assets/stylesheets/common/topic-timeline.scss index 2e2c8e45fd..dcf6afb523 100644 --- a/app/assets/stylesheets/common/topic-timeline.scss +++ b/app/assets/stylesheets/common/topic-timeline.scss @@ -127,6 +127,10 @@ margin-bottom: 0.5em; } + .topic-admin-menu-button-container { + display: flex; + } + .topic-admin-menu-button { display: flex; } diff --git a/app/assets/stylesheets/desktop/topic-list.scss b/app/assets/stylesheets/desktop/topic-list.scss index 1c274156ae..30d21f8ad1 100644 --- a/app/assets/stylesheets/desktop/topic-list.scss +++ b/app/assets/stylesheets/desktop/topic-list.scss @@ -163,12 +163,6 @@ h2 { margin: 20px 0 10px; } - .show-more.has-topics { - top: 0; - .alert { - padding: 16px 35px 16px 14px; - } - } } .bulk-select-topics { diff --git a/app/assets/stylesheets/mobile/new-user.scss b/app/assets/stylesheets/mobile/new-user.scss index 118b6b7ec5..5dbd5aae74 100644 --- a/app/assets/stylesheets/mobile/new-user.scss +++ b/app/assets/stylesheets/mobile/new-user.scss @@ -28,6 +28,62 @@ } } + .user-messages-page & { + .user-navigation-secondary { + display: grid; + grid-template-areas: + "dropdown controls" + "nav-pills nav-pills"; + grid-template-columns: 1fr auto; + font-size: var(--font-up-1); + gap: 0.5em; + + .category-breadcrumb { + grid-area: dropdown; + } + + .horizontal-overflow-nav { + grid-area: nav-pills; + } + + .navigation-controls { + grid-area: controls; + font-size: var(--font-down-2); + + .btn { + padding: 0.5em 1em; + } + + .d-button-label, + .select-kit-header-wrapper .name { + display: none; + } + .d-icon { + margin: 0; + } + } + + .user-nav-messages-dropdown { + max-width: unset; + } + + .combo-box-header { + font-size: var(--font-0); + } + } + + tbody { + border-top-width: 1px; + } + + .user-content { + margin: 0; + table { + margin-top: -1px; // align under nav border + } + } + } + .user-nav-dropdown-list-item { flex-direction: column; } diff --git a/app/assets/stylesheets/mobile/personal-message.scss b/app/assets/stylesheets/mobile/personal-message.scss index d193ed5c96..3dd3f0ebbd 100644 --- a/app/assets/stylesheets/mobile/personal-message.scss +++ b/app/assets/stylesheets/mobile/personal-message.scss @@ -35,19 +35,6 @@ width: 100%; } - .small-action { - margin-left: 0; - } - - .small-action-desc.timegap { - margin-left: 0; - padding: 1em 1em 1em 0; - } - - .small-action:not(.time-gap) { - padding: 1em; - } - .topic-meta-data .names .first.staff { flex-basis: 100%; & + .second, diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index dd4e81e0e4..e5ff07b7da 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -342,18 +342,6 @@ span.highlighted { /* must render on top of topic-body + topic-meta-data, otherwise not tappable */ } -.small-action .topic-avatar { - display: flex; - align-self: stretch; - align-items: flex-start; - margin-right: 0; - float: unset; - height: auto; - .d-icon { - font-size: 1.8em; - } -} - .topic-meta-data { margin-left: 50px; font-size: var(--font-down-1); diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index 2665b2eac0..f55a1ade01 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -433,3 +433,13 @@ margin-top: 1em; } } +.second-factor { + .second-factor-item { + width: auto; + display: flex; + justify-content: space-between; + } + .details { + width: 75%; + } +} diff --git a/app/assets/stylesheets/wizard.scss b/app/assets/stylesheets/wizard.scss index df870f3842..627ae54389 100644 --- a/app/assets/stylesheets/wizard.scss +++ b/app/assets/stylesheets/wizard.scss @@ -206,7 +206,6 @@ body.wizard { display: flex; justify-content: flex-end; position: relative; - margin-top: -1px; padding-right: 10px; .preview-nav-button { text-align: center; @@ -619,15 +618,27 @@ body.wizard { &__checkbox-slider:before, &__checkbox-slider:after { content: ""; + display: block; + position: absolute; + } + + &__checkbox-slider:after { + content: "\2713"; // checkmark + color: var(--secondary); + top: 4px; + left: 10px; + @media only screen and (max-device-width: 568px) { + top: 3px; + left: 5px; + font-size: var(--font-down-3); + } } &__checkbox-slider:before { - display: block; background: var(--secondary); border-radius: 50%; width: 24px; height: 24px; - position: absolute; top: 4px; left: 4px; transition: left 0.25s; @@ -662,6 +673,10 @@ body.wizard { top: 2px; } + label .svg-icon { + top: 2px; + } + .wizard-container__image-upload canvas { border: 1px solid rgba(0, 0, 0, 0.2); } diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 6d2cf1d3fd..7d7526417d 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Admin::UsersController < Admin::StaffController + MAX_SIMILAR_USERS = 10 before_action :fetch_user, only: [:suspend, :unsuspend, @@ -40,7 +41,18 @@ class Admin::UsersController < Admin::StaffController def show @user = User.find_by(id: params[:id]) raise Discourse::NotFound unless @user - render_serialized(@user, AdminDetailedUserSerializer, root: false) + + similar_users = User.real + .where.not(id: @user.id) + .where(ip_address: @user.ip_address) + + render_serialized( + @user, + AdminDetailedUserSerializer, + root: false, + similar_users: similar_users.limit(MAX_SIMILAR_USERS), + similar_users_count: similar_users.count, + ) end def delete_posts_batch @@ -104,44 +116,52 @@ class Admin::UsersController < Admin::StaffController params.require([:suspend_until, :reason]) - @user.suspended_till = params[:suspend_until] - @user.suspended_at = DateTime.now - - message = params[:message] + all_users = [@user] + if Array === params[:other_user_ids] + all_users.concat(User.where(id: params[:other_user_ids]).to_a) + all_users.uniq! + end user_history = nil - User.transaction do - @user.save! + all_users.each do |user| + user.suspended_till = params[:suspend_until] + user.suspended_at = DateTime.now - user_history = StaffActionLogger.new(current_user).log_user_suspend( - @user, - params[:reason], + message = params[:message] + + User.transaction do + user.save! + + user_history = StaffActionLogger.new(current_user).log_user_suspend( + user, + params[:reason], + message: message, + post_id: params[:post_id] + ) + end + user.logged_out + + if message.present? + Jobs.enqueue( + :critical_user_email, + type: "account_suspended", + user_id: user.id, + user_history_id: user_history.id + ) + end + + DiscourseEvent.trigger( + :user_suspended, + user: user, + reason: params[:reason], message: message, - post_id: params[:post_id] + user_history: user_history, + post_id: params[:post_id], + suspended_till: params[:suspend_until], + suspended_at: DateTime.now ) end - @user.logged_out - - if message.present? - Jobs.enqueue( - :critical_user_email, - type: "account_suspended", - user_id: @user.id, - user_history_id: user_history.id - ) - end - - DiscourseEvent.trigger( - :user_suspended, - user: @user, - reason: params[:reason], - message: message, - user_history: user_history, - post_id: params[:post_id], - suspended_till: params[:suspend_until], - suspended_at: DateTime.now - ) perform_post_action @@ -341,31 +361,42 @@ class Admin::UsersController < Admin::StaffController return render json: failed_json.merge(message: message), status: 409 end - message = params[:message] - - silencer = UserSilencer.new( - @user, - current_user, - silenced_till: params[:silenced_till], - reason: params[:reason], - message_body: message, - keep_posts: true, - post_id: params[:post_id] - ) - if silencer.silence - Jobs.enqueue( - :critical_user_email, - type: "account_silenced", - user_id: @user.id, - user_history_id: silencer.user_history.id - ) + all_users = [@user] + if Array === params[:other_user_ids] + all_users.concat(User.where(id: params[:other_user_ids]).to_a) + all_users.uniq! end + + user_history = nil + + all_users.each do |user| + silencer = UserSilencer.new( + user, + current_user, + silenced_till: params[:silenced_till], + reason: params[:reason], + message_body: params[:message], + keep_posts: true, + post_id: params[:post_id] + ) + + if silencer.silence + user_history = silencer.user_history + Jobs.enqueue( + :critical_user_email, + type: "account_silenced", + user_id: user.id, + user_history_id: user_history.id + ) + end + end + perform_post_action render_json_dump( silence: { silenced: true, - silence_reason: silencer.user_history.try(:details), + silence_reason: user_history.try(:details), silenced_till: @user.silenced_till, silenced_at: @user.silenced_at, silenced_by: BasicUserSerializer.new(current_user, root: false).as_json diff --git a/app/controllers/admin/web_hooks_controller.rb b/app/controllers/admin/web_hooks_controller.rb index c4889a47dd..cb747112bd 100644 --- a/app/controllers/admin/web_hooks_controller.rb +++ b/app/controllers/admin/web_hooks_controller.rb @@ -32,6 +32,10 @@ class Admin::WebHooksController < Admin::AdminController render_serialized(@web_hook, AdminWebHookSerializer, root: 'web_hook') end + def edit + render_serialized(@web_hook, AdminWebHookSerializer, root: 'web_hook') + end + def create web_hook = WebHook.new(web_hook_params) @@ -58,9 +62,6 @@ class Admin::WebHooksController < Admin::AdminController render json: success_json end - def new - end - def list_events limit = 50 offset = params[:offset].to_i diff --git a/app/controllers/composer_controller.rb b/app/controllers/composer_controller.rb new file mode 100644 index 0000000000..fd4847e115 --- /dev/null +++ b/app/controllers/composer_controller.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true + +class ComposerController < ApplicationController + requires_login + + def mentions + @names = params.require(:names) + raise Discourse::InvalidParameters.new(:names) if !@names.kind_of?(Array) || @names.size > 20 + + if params[:topic_id].present? + @topic = Topic.find_by(id: params[:topic_id]) + guardian.ensure_can_see!(@topic) + end + + # allowed_names is necessary just for new private messages. + @allowed_names = if params[:allowed_names].present? + raise Discourse::InvalidParameters(:allowed_names) if !params[:allowed_names].is_a?(Array) + params[:allowed_names] << current_user.username + else + [] + end + + user_reasons = {} + group_reasons = {} + @names.each do |name| + if user = users[name] + reason = user_reason(user) + user_reasons[name] = reason if reason.present? + elsif group = groups[name] + reason = group_reason(group) + group_reasons[name] = reason if reason.present? + end + end + + if @topic && @names.include?(SiteSetting.here_mention) && guardian.can_mention_here? + here_count = PostAlerter.new.expand_here_mention(@topic.first_post, exclude_ids: [current_user.id]).size + end + + serialized_groups = groups.values.reduce({}) do |hash, group| + serialized_group = { user_count: group.user_count } + + if group_reasons[group.name] == :not_allowed && + members_visible_group_ids.include?(group.id) && + (@topic&.private_message? || @allowed_names.present?) + + # Find users that are notified already because they have been invited + # directly or via a group + notified_count = GroupUser + # invited directly + .where(user_id: topic_allowed_user_ids) + .or( + # invited via a group + GroupUser.where( + user_id: GroupUser.where(group_id: topic_allowed_group_ids).select(:user_id) + ) + ) + .where(group_id: group.id) + .select(:user_id).distinct.count + + if notified_count > 0 + group_reasons[group.name] = :some_not_allowed + serialized_group[:notified_count] = notified_count + end + end + + hash[group.name] = serialized_group + hash + end + + render json: { + users: users.keys, + user_reasons: user_reasons, + groups: serialized_groups, + group_reasons: group_reasons, + here_count: here_count, + max_users_notified_per_group_mention: SiteSetting.max_users_notified_per_group_mention, + } + end + + private + + def user_reason(user) + reason = if @topic && !user.guardian.can_see?(@topic) + @topic.private_message? ? :private : :category + elsif @allowed_names.present? && !is_user_allowed?(user, topic_allowed_user_ids, topic_allowed_group_ids) + # This would normally be handled by the previous if, but that does not work for new private messages. + :private + elsif topic_muted_by.include?(user.id) + :muted_topic + elsif @topic&.private_message? && !is_user_allowed?(user, topic_allowed_user_ids, topic_allowed_group_ids) + # Admins can see the topic, but they will not be mentioned if they were not invited. + :not_allowed + end + + # Regular users can see only basic information why the users cannot see the topic. + reason = nil if !guardian.is_staff? && reason != :private && reason != :category + + reason + end + + def group_reason(group) + if !mentionable_group_ids.include?(group.id) + :not_mentionable + elsif (@topic&.private_message? || @allowed_names.present?) && !topic_allowed_group_ids.include?(group.id) + :not_allowed + end + end + + def is_user_allowed?(user, user_ids, group_ids) + user_ids.include?(user.id) || user.group_ids.any? { |group_id| group_ids.include?(group_id) } + end + + def users + @users ||= User + .not_staged + .where(username_lower: @names.map(&:downcase)) + .index_by(&:username_lower) + end + + def groups + @groups ||= Group + .visible_groups(current_user) + .where('lower(name) IN (?)', @names.map(&:downcase)) + .index_by(&:name) + end + + def mentionable_group_ids + @mentionable_group_ids ||= Group + .mentionable(current_user, include_public: false) + .where(name: @names) + .pluck(:id) + .to_set + end + + def members_visible_group_ids + @members_visible_group_ids ||= Group + .members_visible_groups(current_user) + .where(name: @names) + .pluck(:id) + .to_set + end + + def topic_muted_by + @topic_muted_by ||= if @topic.present? + TopicUser + .where(topic: @topic) + .where(user_id: users.values.map(&:id)) + .where(notification_level: TopicUser.notification_levels[:muted]) + .pluck(:user_id) + .to_set + else + Set.new + end + end + + def topic_allowed_user_ids + @topic_allowed_user_ids ||= if @allowed_names.present? + User + .where(username_lower: @allowed_names.map(&:downcase)) + .pluck(:id) + .to_set + elsif @topic&.private_message? + TopicAllowedUser + .where(topic: @topic) + .pluck(:user_id) + .to_set + end + end + + def topic_allowed_group_ids + @topic_allowed_group_ids ||= if @allowed_names.present? + Group + .messageable(current_user) + .where(name: @allowed_names) + .pluck(:id) + .to_set + elsif @topic&.private_message? + TopicAllowedGroup + .where(topic: @topic) + .pluck(:group_id) + .to_set + end + end +end diff --git a/app/controllers/hashtags_controller.rb b/app/controllers/hashtags_controller.rb index b6cf86df59..e55b684022 100644 --- a/app/controllers/hashtags_controller.rb +++ b/app/controllers/hashtags_controller.rb @@ -12,7 +12,6 @@ class HashtagsController < ApplicationController end def search - params.require(:term) params.require(:order) results = HashtagAutocompleteService.new(guardian).search(params[:term], params[:order]) diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index ca7e548a70..2006ddeddd 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -31,12 +31,13 @@ class NotificationsController < ApplicationController limit = 50 if limit > 50 include_reviewables = false - if SiteSetting.enable_experimental_sidebar_hamburger + + if SiteSetting.legacy_navigation_menu? + notifications = Notification.recent_report(current_user, limit, notification_types) + else notifications = Notification.prioritized_list(current_user, count: limit, types: notification_types) # notification_types is blank for the "all notifications" user menu tab include_reviewables = notification_types.blank? && guardian.can_see_review_queue? - else - notifications = Notification.recent_report(current_user, limit, notification_types) end if notifications.present? && !(params.has_key?(:silent) || @readonly_mode) @@ -63,12 +64,14 @@ class NotificationsController < ApplicationController notifications: serialize_data(notifications, NotificationSerializer), seen_notification_id: current_user.seen_notification_id } + if include_reviewables json[:pending_reviewables] = Reviewable.basic_serializers_for_list( Reviewable.user_menu_list_for(current_user), current_user ).as_json end + render_json_dump(json) else offset = params[:offset].to_i diff --git a/app/controllers/reviewable_claimed_topics_controller.rb b/app/controllers/reviewable_claimed_topics_controller.rb index 1669c3751d..2a6033118b 100644 --- a/app/controllers/reviewable_claimed_topics_controller.rb +++ b/app/controllers/reviewable_claimed_topics_controller.rb @@ -51,7 +51,8 @@ class ReviewableClaimedTopicsController < ApplicationController end MessageBus.publish("/reviewable_claimed", data, group_ids: group_ids.to_a) - if SiteSetting.enable_experimental_sidebar_hamburger + + if !SiteSetting.legacy_navigation_menu? Jobs.enqueue(:refresh_users_reviewable_counts, group_ids: group_ids.to_a) end end diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 67c844dbe6..d2b3caeb24 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -497,6 +497,10 @@ class TopicsController < ApplicationController topic = Topic.find_by(id: params[:topic_id]) guardian.ensure_can_moderate!(topic) + if TopicTimer.destructive_types.values.include?(status_type) + guardian.ensure_can_delete!(topic) + end + options = { by_user: current_user, based_on_last_post: based_on_last_post diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 3875747d5e..1cc1d0e2b4 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -497,93 +497,6 @@ class UsersController < ApplicationController end end - def is_local_username - usernames = params[:usernames] if params[:usernames].present? - usernames = [params[:username]] if params[:username].present? - - raise Discourse::InvalidParameters.new(:usernames) if !usernames.kind_of?(Array) || usernames.size > 20 - - groups = Group.where(name: usernames).pluck(:name) - mentionable_groups = - if current_user - Group.mentionable(current_user, include_public: false) - .where(name: usernames) - .pluck(:name, :user_count) - .map do |name, user_count| - { - name: name, - user_count: user_count - } - end - end - - usernames -= groups - usernames.each(&:downcase!) - - users = User - .where(staged: false, username_lower: usernames) - .index_by(&:username_lower) - - cannot_see = {} - here_count = nil - - topic_id = params[:topic_id] - if topic_id.present? && topic = Topic.find_by(id: topic_id) - topic_muted_by = TopicUser - .where(topic: topic) - .where(user_id: users.values.map(&:id)) - .where(notification_level: TopicUser.notification_levels[:muted]) - .pluck(:user_id) - .to_set - - if topic.private_message? - topic_allowed_user_ids = TopicAllowedUser - .where(topic: topic) - .where(user_id: users.values.map(&:id)) - .pluck(:user_id) - .to_set - - topic_allowed_group_ids = TopicAllowedGroup - .where(topic: topic) - .pluck(:group_id) - .to_set - end - - usernames.each do |username| - user = users[username] - next if user.blank? - - cannot_see_reason = nil - if !user.guardian.can_see?(topic) - cannot_see_reason = topic.private_message? ? :private : :category - elsif topic_muted_by.include?(user.id) - cannot_see_reason = :muted_topic - elsif topic.private_message? && !topic_allowed_user_ids.include?(user.id) && !user.group_ids.any? { |group_id| topic_allowed_group_ids.include?(group_id) } - cannot_see_reason = :not_allowed - end - - if !guardian.is_staff? && cannot_see_reason.present? && cannot_see_reason != :private && cannot_see_reason != :category - cannot_see_reason = nil # do not leak private information - end - - cannot_see[username] = cannot_see_reason if cannot_see_reason.present? - end - - if usernames.include?(SiteSetting.here_mention) && guardian.can_mention_here? - here_count = PostAlerter.new.expand_here_mention(topic.first_post, exclude_ids: [current_user.id]).size - end - end - - render json: { - valid: users.keys, - valid_groups: groups, - mentionable_groups: mentionable_groups, - cannot_see: cannot_see, - here_count: here_count, - max_users_notified_per_group_mention: SiteSetting.max_users_notified_per_group_mention - } - end - def render_available_true render(json: { available: true }) end @@ -1957,7 +1870,7 @@ class UsersController < ApplicationController permitted.concat UserUpdater::TAG_NAMES.keys permitted << UserUpdater::NOTIFICATION_SCHEDULE_ATTRS - if SiteSetting.enable_experimental_sidebar_hamburger + if !SiteSetting.legacy_navigation_menu? if params.has_key?(:sidebar_category_ids) && params[:sidebar_category_ids].blank? params[:sidebar_category_ids] = [] end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 96d3c11a65..06e8be2145 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -95,9 +95,10 @@ module ApplicationHelper path = ActionController::Base.helpers.asset_path("#{script}.js") if GlobalSetting.use_s3? && GlobalSetting.s3_cdn_url + resolved_s3_asset_cdn_url = GlobalSetting.s3_asset_cdn_url.presence || GlobalSetting.s3_cdn_url if GlobalSetting.cdn_url folder = ActionController::Base.config.relative_url_root || "/" - path = path.gsub(File.join(GlobalSetting.cdn_url, folder, "/"), File.join(GlobalSetting.s3_cdn_url, "/")) + path = path.gsub(File.join(GlobalSetting.cdn_url, folder, "/"), File.join(resolved_s3_asset_cdn_url, "/")) else # we must remove the subfolder path here, assets are uploaded to s3 # without it getting involved @@ -105,7 +106,7 @@ module ApplicationHelper path = path.sub(ActionController::Base.config.relative_url_root, "") end - path = "#{GlobalSetting.s3_cdn_url}#{path}" + path = "#{resolved_s3_asset_cdn_url}#{path}" end # assets needed for theme testing are not compressed because they take a fair diff --git a/app/jobs/regular/notify_reviewable.rb b/app/jobs/regular/notify_reviewable.rb index cbfc37431d..5b71ac557e 100644 --- a/app/jobs/regular/notify_reviewable.rb +++ b/app/jobs/regular/notify_reviewable.rb @@ -31,31 +31,31 @@ class Jobs::NotifyReviewable < ::Jobs::Base counts[r.reviewable_by_group_id] += 1 if r.reviewable_by_group_id end - if SiteSetting.enable_experimental_sidebar_hamburger - notify_users( - User.real.admins, - all_updates[:admins] - ) - else + if SiteSetting.legacy_navigation_menu? notify_legacy( User.real.admins.pluck(:id), count: counts[:admins], updates: all_updates[:admins], ) + else + notify_users( + User.real.admins, + all_updates[:admins] + ) end if reviewable.reviewable_by_moderator? - if SiteSetting.enable_experimental_sidebar_hamburger - notify_users( - User.real.moderators.where("id NOT IN (?)", @contacted), - all_updates[:moderators] - ) - else + if SiteSetting.legacy_navigation_menu? notify_legacy( User.real.moderators.where("id NOT IN (?)", @contacted).pluck(:id), count: counts[:moderators], updates: all_updates[:moderators], ) + else + notify_users( + User.real.moderators.where("id NOT IN (?)", @contacted), + all_updates[:moderators] + ) end end @@ -70,10 +70,10 @@ class Jobs::NotifyReviewable < ::Jobs::Base count += counts[gu.group_id] end - if SiteSetting.enable_experimental_sidebar_hamburger - notify_user(user, updates) - else + if SiteSetting.legacy_navigation_menu? notify_legacy([user.id], count: count, updates: updates) + else + notify_user(user, updates) end end diff --git a/app/models/category.rb b/app/models/category.rb index 7e7bd947b0..477744f987 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -202,6 +202,10 @@ class Category < ActiveRecord::Base Category.clear_subcategory_ids end + def top_level? + self.parent_category_id.nil? + end + def self.scoped_to_permissions(guardian, permission_types) if guardian.try(:is_admin?) all diff --git a/app/models/concerns/category_hashtag.rb b/app/models/concerns/category_hashtag.rb index 959216baf3..db28efe789 100644 --- a/app/models/concerns/category_hashtag.rb +++ b/app/models/concerns/category_hashtag.rb @@ -12,9 +12,7 @@ module CategoryHashtag slug_path = category_slug.split(SEPARATOR) return nil if slug_path.empty? || slug_path.size > 2 - if SiteSetting.slug_generation_method == "encoded" - slug_path.map! { |slug| CGI.escape(slug) } - end + slug_path.map! { |slug| CGI.escape(slug) } if SiteSetting.slug_generation_method == "encoded" parent_slug, child_slug = slug_path.last(2) categories = Category.where(slug: parent_slug) @@ -31,9 +29,8 @@ module CategoryHashtag # depth supported). # # @param {Array} category_slugs - Slug strings to look up, can also be in the parent:child format - # @param {Array} cached_categories - An array of Hashes representing categories, Site.categories - # should be used here since it is scoped to the Guardian. - def query_from_cached_categories(category_slugs, cached_categories) + # @param {Array} categories - An array of Category models scoped to the user's guardian permissions. + def query_loaded_from_slugs(category_slugs, categories) category_slugs .map(&:downcase) .map do |slug| @@ -51,18 +48,19 @@ module CategoryHashtag # by its slug then find the child by its slug and its parent's # ID to make sure they match. if child_slug.present? - parent_category = cached_categories.find { |cat| cat[:slug].downcase == parent_slug } + parent_category = categories.find { |cat| cat.slug.casecmp?(parent_slug) } if parent_category.present? - cached_categories.find do |cat| - cat[:slug].downcase == child_slug && cat[:parent_category_id] == parent_category[:id] + categories.find do |cat| + cat.slug.downcase == child_slug && cat.parent_category_id == parent_category.id end end else - cached_categories.find do |cat| - cat[:slug].downcase == parent_slug && cat[:parent_category_id].nil? + categories.find do |cat| + cat.slug.downcase == parent_slug && cat.top_level? end end - end.compact + end + .compact end end end diff --git a/app/models/concerns/second_factor_manager.rb b/app/models/concerns/second_factor_manager.rb index 4e212306af..e21957e75f 100644 --- a/app/models/concerns/second_factor_manager.rb +++ b/app/models/concerns/second_factor_manager.rb @@ -79,7 +79,7 @@ module SecondFactorManager end def has_any_second_factor_methods_enabled? - totp_enabled? || security_keys_enabled? || backup_codes_enabled? + totp_enabled? || security_keys_enabled? end def has_multiple_second_factor_methods? diff --git a/app/models/navigation_menu_site_setting.rb b/app/models/navigation_menu_site_setting.rb new file mode 100644 index 0000000000..d71cac2fdd --- /dev/null +++ b/app/models/navigation_menu_site_setting.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class NavigationMenuSiteSetting < EnumSiteSetting + SIDEBAR = "sidebar" + HEADER_DROPDOWN = "header dropdown" + LEGACY = "legacy" + + def self.valid_value?(val) + values.any? { |v| v[:value] == val } + end + + def self.values + @values ||= [ + { name: "admin.navigation_menu.sidebar", value: SIDEBAR }, + { name: "admin.navigation_menu.header_dropdown", value: HEADER_DROPDOWN }, + { name: "admin.navigation_menu.legacy", value: LEGACY } + ] + end + + def self.translate_names? + true + end +end diff --git a/app/models/post.rb b/app/models/post.rb index 39aab5700d..7d1f551414 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -1128,6 +1128,10 @@ class Post < ActiveRecord::Base end end + def mentions + PrettyText.extract_mentions(Nokogiri::HTML5.fragment(cooked)) + end + private def parse_quote_into_arguments(quote) diff --git a/app/models/post_analyzer.rb b/app/models/post_analyzer.rb index c049f80c39..bbd5bdd7f3 100644 --- a/app/models/post_analyzer.rb +++ b/app/models/post_analyzer.rb @@ -78,18 +78,7 @@ class PostAnalyzer def raw_mentions return [] if @raw.blank? return @raw_mentions if @raw_mentions.present? - - raw_mentions = cooked_stripped.css('.mention, .mention-group').map do |e| - if name = e.inner_text - name = name[1..-1] - name = User.normalize_username(name) - name - end - end - - raw_mentions.compact! - raw_mentions.uniq! - @raw_mentions = raw_mentions + @raw_mentions = PrettyText.extract_mentions(cooked_stripped) end # from rack ... compat with ruby 2.2 diff --git a/app/models/screened_ip_address.rb b/app/models/screened_ip_address.rb index 70d51c85ec..13bf4a9092 100644 --- a/app/models/screened_ip_address.rb +++ b/app/models/screened_ip_address.rb @@ -11,7 +11,7 @@ class ScreenedIpAddress < ActiveRecord::Base default_action :block validates :ip_address, ip_address_format: true, presence: true - after_validation :check_for_match + after_validation :check_for_match, if: :will_save_change_to_ip_address? ROLLED_UP_BLOCKS = [ # IPv4 diff --git a/app/models/site.rb b/app/models/site.rb index e9594d6e25..2899a932cb 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -134,7 +134,7 @@ class Site categories.reject! { |c| c[:parent_category_id] && !by_id[c[:parent_category_id]] } self.class.categories_callbacks.each do |callback| - callback.call(categories) + callback.call(categories, @guardian) end categories diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index 7ec4016dc1..b87146e52d 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -237,6 +237,10 @@ class SiteSetting < ActiveRecord::Base c.present? && c.to_i != SiteSetting.uncategorized_category_id.to_i end + def self.legacy_navigation_menu? + SiteSetting.navigation_menu == "legacy" + end + ALLOWLIST_DEPRECATED_SITE_SETTINGS = { 'email_domains_blacklist': 'blocked_email_domains', 'email_domains_whitelist': 'allowed_email_domains', diff --git a/app/models/theme.rb b/app/models/theme.rb index ef34ef6a94..3c991a6445 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -6,7 +6,7 @@ require 'json_schemer' class Theme < ActiveRecord::Base include GlobalPath - BASE_COMPILER_VERSION = 68 + BASE_COMPILER_VERSION = 69 attr_accessor :child_components diff --git a/app/models/topic.rb b/app/models/topic.rb index 2db3023458..8c407873e1 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -131,25 +131,35 @@ class Topic < ActiveRecord::Base end def trash!(trashed_by = nil) + trigger_event = false + if deleted_at.nil? update_category_topic_count_by(-1) if visible? CategoryTagStat.topic_deleted(self) if self.tags.present? - DiscourseEvent.trigger(:topic_trashed, self) + trigger_event = true end + super(trashed_by) + + DiscourseEvent.trigger(:topic_trashed, self) if trigger_event + self.topic_embed.trash! if has_topic_embed? end def recover!(recovered_by = nil) + trigger_event = false + unless deleted_at.nil? update_category_topic_count_by(1) if visible? CategoryTagStat.topic_recovered(self) if self.tags.present? - DiscourseEvent.trigger(:topic_recovered, self) + trigger_event = true end # Note parens are required because superclass doesn't take `recovered_by` super() + DiscourseEvent.trigger(:topic_recovered, self) if trigger_event + unless (topic_embed = TopicEmbed.with_deleted.find_by_topic_id(id)).nil? topic_embed.recover! end @@ -624,14 +634,27 @@ class Topic < ActiveRecord::Base tsquery = Search.to_tsquery(term: tsquery, joiner: "|") + guardian = Guardian.new(user) + + excluded_category_ids_sql = Category.secured(guardian).where(search_priority: Searchable::PRIORITIES[:ignore]).select(:id).to_sql + + if user + excluded_category_ids_sql = <<~SQL + #{excluded_category_ids_sql} + UNION + #{CategoryUser.muted_category_ids_query(user, include_direct: true).select("categories.id").to_sql} + SQL + end + candidates = Topic .visible .listable_topics - .secured(Guardian.new(user)) + .secured(guardian) .joins("JOIN topic_search_data s ON topics.id = s.topic_id") .joins("LEFT JOIN categories c ON topics.id = c.topic_id") .where("search_data @@ #{tsquery}") .where("c.topic_id IS NULL") + .where("topics.category_id NOT IN (#{excluded_category_ids_sql})") .order("ts_rank(search_data, #{tsquery}) DESC") .limit(SiteSetting.max_similar_results * 3) diff --git a/app/models/topic_list.rb b/app/models/topic_list.rb index d13c2ab3ec..815e9c2308 100644 --- a/app/models/topic_list.rb +++ b/app/models/topic_list.rb @@ -136,8 +136,11 @@ class TopicList ft.topic_list = self end + topic_preloader_associations = [:image_upload, { topic_thumbnails: :optimized_image }] + topic_preloader_associations.concat(DiscoursePluginRegistry.topic_preloader_associations.to_a) + ActiveRecord::Associations::Preloader - .new(records: @topics, associations: [:image_upload, topic_thumbnails: :optimized_image]) + .new(records: @topics, associations: topic_preloader_associations) .call if preloaded_custom_fields.present? diff --git a/app/models/topic_timer.rb b/app/models/topic_timer.rb index 05f0226fd1..82a00e8dc3 100644 --- a/app/models/topic_timer.rb +++ b/app/models/topic_timer.rb @@ -98,6 +98,10 @@ class TopicTimer < ActiveRecord::Base @_private_types ||= types.only(:reminder, :clear_slow_mode) end + def self.destructive_types + @_destructive_types ||= types.only(:delete, :delete_replies) + end + def public_type? !!self.class.public_types[self.status_type] end diff --git a/app/models/topic_tracking_state.rb b/app/models/topic_tracking_state.rb index bc25cf9281..65bf2fa59c 100644 --- a/app/models/topic_tracking_state.rb +++ b/app/models/topic_tracking_state.rb @@ -276,7 +276,7 @@ class TopicTrackingState end def self.include_tags_in_report? - SiteSetting.tagging_enabled && (@include_tags_in_report || SiteSetting.enable_experimental_sidebar_hamburger) + SiteSetting.tagging_enabled && (@include_tags_in_report || !SiteSetting.legacy_navigation_menu?) end def self.include_tags_in_report=(v) diff --git a/app/models/user.rb b/app/models/user.rb index 79a6fc22d6..605638f390 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -85,7 +85,7 @@ class User < ActiveRecord::Base has_many :topics_allowed, through: :topic_allowed_users, source: :topic has_many :groups, through: :group_users - has_many :secure_categories, through: :groups, source: :categories + has_many :secure_categories, -> { distinct }, through: :groups, source: :categories has_many :associated_groups, through: :user_associated_groups, dependent: :destroy # deleted in user_second_factors relationship @@ -135,10 +135,14 @@ class User < ActiveRecord::Base after_create :ensure_in_trust_level_group after_create :set_default_categories_preferences after_create :set_default_tags_preferences - after_create :set_default_sidebar_section_links + after_create :add_default_sidebar_section_links - after_update :set_default_sidebar_section_links, if: Proc.new { - self.saved_change_to_staged? || self.saved_change_to_admin? + after_update :update_default_sidebar_section_links, if: Proc.new { + self.saved_change_to_admin? + } + + after_update :add_default_sidebar_section_links, if: Proc.new { + self.saved_change_to_staged? } after_update :trigger_user_updated_event, if: Proc.new { @@ -1715,7 +1719,7 @@ class User < ActiveRecord::Base end def redesigned_user_menu_enabled? - SiteSetting.enable_experimental_sidebar_hamburger + !SiteSetting.legacy_navigation_menu? end protected @@ -1946,25 +1950,54 @@ class User < ActiveRecord::Base private - def set_default_sidebar_section_links - return if !SiteSetting.enable_experimental_sidebar_hamburger + def set_default_sidebar_section_links(update: false) + return if SiteSetting.legacy_navigation_menu? return if staged? || bot? if SiteSetting.default_sidebar_categories.present? + categories_to_update = SiteSetting.default_sidebar_categories.split("|") + + if update + filtered_default_category_ids = Category.secured(self.guardian).where(id: categories_to_update).pluck(:id) + existing_category_ids = SidebarSectionLink.where(user: self, linkable_type: 'Category').pluck(:linkable_id) + + categories_to_update = existing_category_ids + (filtered_default_category_ids & self.secure_category_ids) + end + SidebarSectionLinksUpdater.update_category_section_links( self, - category_ids: SiteSetting.default_sidebar_categories.split("|") + category_ids: categories_to_update ) end if SiteSetting.tagging_enabled && SiteSetting.default_sidebar_tags.present? + tags_to_update = SiteSetting.default_sidebar_tags.split("|") + + if update + default_tag_ids = Tag.where(name: tags_to_update).pluck(:id) + filtered_default_tags = DiscourseTagging.filter_visible(Tag, self.guardian).where(id: default_tag_ids).pluck(:name) + + existing_tag_ids = SidebarSectionLink.where(user: self, linkable_type: 'Tag').pluck(:linkable_id) + existing_tags = DiscourseTagging.filter_visible(Tag, self.guardian).where(id: existing_tag_ids).pluck(:name) + + tags_to_update = existing_tags + (filtered_default_tags & DiscourseTagging.hidden_tag_names) + end + SidebarSectionLinksUpdater.update_tag_section_links( self, - tag_names: SiteSetting.default_sidebar_tags.split("|") + tag_names: tags_to_update ) end end + def add_default_sidebar_section_links + set_default_sidebar_section_links + end + + def update_default_sidebar_section_links + set_default_sidebar_section_links(update: true) + end + def stat user_stat || create_user_stat end diff --git a/app/models/user_api_key_scope.rb b/app/models/user_api_key_scope.rb index 6b54d3ca65..d2f0c408a3 100644 --- a/app/models/user_api_key_scope.rb +++ b/app/models/user_api_key_scope.rb @@ -16,7 +16,12 @@ class UserApiKeyScope < ActiveRecord::Base RouteMatcher.new(methods: :get, actions: 'session#current'), RouteMatcher.new(methods: :get, actions: 'users#topic_tracking_state') ], - bookmarks_calendar: [ RouteMatcher.new(methods: :get, actions: 'users#bookmarks', formats: :ics, params: %i[username]) ] + bookmarks_calendar: [ RouteMatcher.new(methods: :get, actions: 'users#bookmarks', formats: :ics, params: %i[username]) ], + user_status: [ + RouteMatcher.new(methods: :get, actions: 'user_status#get'), + RouteMatcher.new(methods: :put, actions: 'user_status#set'), + RouteMatcher.new(methods: :delete, actions: 'user_status#clear') + ] } def self.all_scopes @@ -36,7 +41,6 @@ class UserApiKeyScope < ActiveRecord::Base def matchers @matchers ||= Array(self.class.all_scopes[name.to_sym]) end - end # == Schema Information diff --git a/app/serializers/admin_detailed_user_serializer.rb b/app/serializers/admin_detailed_user_serializer.rb index 454fcf6cbd..ec0d682d57 100644 --- a/app/serializers/admin_detailed_user_serializer.rb +++ b/app/serializers/admin_detailed_user_serializer.rb @@ -36,7 +36,9 @@ class AdminDetailedUserSerializer < AdminUserSerializer :can_disable_second_factor, :can_delete_sso_record, :api_key_count, - :external_ids + :external_ids, + :similar_users, + :similar_users_count has_one :approved_by, serializer: BasicUserSerializer, embed: :objects has_one :suspended_by, serializer: BasicUserSerializer, embed: :objects @@ -45,7 +47,7 @@ class AdminDetailedUserSerializer < AdminUserSerializer has_many :groups, embed: :object, serializer: BasicGroupSerializer def second_factor_enabled - object.totp_enabled? || object.security_keys_enabled? || object.backup_codes_enabled? + object.totp_enabled? || object.security_keys_enabled? end def can_disable_second_factor @@ -156,6 +158,28 @@ class AdminDetailedUserSerializer < AdminUserSerializer external_ids end + def similar_users + ActiveModel::ArraySerializer.new( + @options[:similar_users], + each_serializer: AdminUserListSerializer, + each_serializer: SimilarAdminUserSerializer, + scope: scope, + root: false, + ).as_json + end + + def include_similar_users? + @options[:similar_users].present? + end + + def similar_users_count + @options[:similar_users_count] + end + + def include_similar_users_count? + @options[:similar_users].present? + end + def can_delete_sso_record scope.can_delete_sso_record?(object) end diff --git a/app/serializers/concerns/user_sidebar_tags_mixin.rb b/app/serializers/concerns/user_sidebar_tags_mixin.rb index 05fa60afda..bcdfc3c93f 100644 --- a/app/serializers/concerns/user_sidebar_tags_mixin.rb +++ b/app/serializers/concerns/user_sidebar_tags_mixin.rb @@ -26,6 +26,6 @@ module UserSidebarTagsMixin end def include_display_sidebar_tags? - SiteSetting.tagging_enabled && SiteSetting.enable_experimental_sidebar_hamburger + SiteSetting.tagging_enabled && !SiteSetting.legacy_navigation_menu? end end diff --git a/app/serializers/current_user_option_serializer.rb b/app/serializers/current_user_option_serializer.rb new file mode 100644 index 0000000000..edfabcf3d6 --- /dev/null +++ b/app/serializers/current_user_option_serializer.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class CurrentUserOptionSerializer < ApplicationSerializer + attributes :mailing_list_mode, + :external_links_in_new_tab, + :enable_quoting, + :dynamic_favicon, + :automatically_unpin_topics, + :likes_notifications_disabled, + :hide_profile_and_presence, + :title_count_mode, + :enable_defer, + :timezone, + :skip_new_user_tips, + :default_calendar, + :bookmark_auto_delete_preference, + :seen_popups, + :should_be_redirected_to_top, + :redirected_to_top, + :treat_as_new_topic_start_date, + + def likes_notifications_disabled + object.likes_notifications_disabled? + end + + def include_redirected_to_top? + object.redirected_to_top.present? + end + + def include_seen_popups? + SiteSetting.enable_user_tips + end +end diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index 278f432965..6a6f7f8feb 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -17,10 +17,6 @@ class CurrentUserSerializer < BasicUserSerializer :whisperer?, :title, :any_posts, - :enable_quoting, - :enable_defer, - :external_links_in_new_tab, - :dynamic_favicon, :trust_level, :can_send_private_email_messages, :can_send_private_messages, @@ -28,8 +24,6 @@ class CurrentUserSerializer < BasicUserSerializer :can_invite_to_forum, :no_password, :can_delete_account, - :should_be_redirected_to_top, - :redirected_to_top, :custom_fields, :muted_category_ids, :indirectly_muted_category_ids, @@ -48,9 +42,6 @@ class CurrentUserSerializer < BasicUserSerializer :unseen_reviewable_count, :new_personal_messages_notifications_count, :read_faq?, - :automatically_unpin_topics, - :mailing_list_mode, - :treat_as_new_topic_start_date, :previous_visit_at, :seen_notification_id, :primary_group_id, @@ -61,25 +52,17 @@ class CurrentUserSerializer < BasicUserSerializer :external_id, :associated_account_ids, :top_category_ids, - :hide_profile_and_presence, :groups, :second_factor_enabled, :ignored_users, - :title_count_mode, - :timezone, :featured_topic, - :skip_new_user_tips, - :seen_popups, :do_not_disturb_until, :has_topic_draft, :can_review, :draft_count, - :default_calendar, - :bookmark_auto_delete_preference, :pending_posts_count, :status, :sidebar_category_ids, - :likes_notifications_disabled, :grouped_unread_notifications, :redesigned_user_menu_enabled, :redesigned_user_page_nav_enabled, @@ -89,6 +72,8 @@ class CurrentUserSerializer < BasicUserSerializer delegate :user_stat, to: :object, private: true delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat + has_one :user_option, embed: :object, serializer: CurrentUserOptionSerializer + def groups owned_group_ids = GroupUser.where(user_id: id, owner: true).pluck(:group_id).to_set @@ -115,54 +100,6 @@ class CurrentUserSerializer < BasicUserSerializer scope.can_create_group? end - def hide_profile_and_presence - object.user_option.hide_profile_and_presence - end - - def enable_quoting - object.user_option.enable_quoting - end - - def enable_defer - object.user_option.enable_defer - end - - def external_links_in_new_tab - object.user_option.external_links_in_new_tab - end - - def dynamic_favicon - object.user_option.dynamic_favicon - end - - def title_count_mode - object.user_option.title_count_mode - end - - def automatically_unpin_topics - object.user_option.automatically_unpin_topics - end - - def should_be_redirected_to_top - object.user_option.should_be_redirected_to_top - end - - def redirected_to_top - object.user_option.redirected_to_top - end - - def timezone - object.user_option.timezone - end - - def default_calendar - object.user_option.default_calendar - end - - def bookmark_auto_delete_preference - object.user_option.bookmark_auto_delete_preference - end - def sidebar_list_destination object.user_option.sidebar_list_none_selected? ? SiteSetting.default_sidebar_list_destination : object.user_option.sidebar_list_destination end @@ -203,10 +140,6 @@ class CurrentUserSerializer < BasicUserSerializer true end - def include_redirected_to_top? - object.user_option.redirected_to_top.present? - end - def custom_fields fields = nil if SiteSetting.public_user_custom_fields.present? @@ -278,26 +211,6 @@ class CurrentUserSerializer < BasicUserSerializer scope.can_see_review_queue? end - def mailing_list_mode - object.user_option.mailing_list_mode - end - - def treat_as_new_topic_start_date - object.user_option.treat_as_new_topic_start_date - end - - def skip_new_user_tips - object.user_option.skip_new_user_tips - end - - def seen_popups - object.user_option.seen_popups - end - - def include_seen_popups? - SiteSetting.enable_user_tips - end - def include_primary_group_id? object.primary_group_id.present? end @@ -325,7 +238,7 @@ class CurrentUserSerializer < BasicUserSerializer end def second_factor_enabled - object.totp_enabled? || object.security_keys_enabled? || object.backup_codes_enabled? + object.totp_enabled? || object.security_keys_enabled? end def featured_topic @@ -345,7 +258,7 @@ class CurrentUserSerializer < BasicUserSerializer end def include_sidebar_category_ids? - SiteSetting.enable_experimental_sidebar_hamburger + !SiteSetting.legacy_navigation_menu? end def include_status? @@ -364,10 +277,6 @@ class CurrentUserSerializer < BasicUserSerializer object.redesigned_user_menu_enabled? end - def likes_notifications_disabled - object.user_option&.likes_notifications_disabled? - end - def include_all_unread_notifications_count? redesigned_user_menu_enabled end diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index 8f0bebf4c5..6ec5f549f2 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -88,7 +88,8 @@ class PostSerializer < BasicPostSerializer :reviewable_score_count, :reviewable_score_pending_count, :user_suspended, - :user_status + :user_status, + :mentioned_users def initialize(object, opts) super(object, opts) @@ -560,6 +561,17 @@ class PostSerializer < BasicPostSerializer UserStatusSerializer.new(object.user&.user_status, root: false) end + def mentioned_users + if @topic_view && (mentions = @topic_view.mentions[object.id]) + return mentions + .map { |username| @topic_view.mentioned_users[username] } + .compact + .map { |user| BasicUserWithStatusSerializer.new(user, root: false) } + end + + [] + end + private def can_review_topic? @@ -590,5 +602,4 @@ private def post_actions @post_actions ||= (@topic_view&.all_post_actions || {})[object.id] end - end diff --git a/app/serializers/similar_admin_user_serializer.rb b/app/serializers/similar_admin_user_serializer.rb new file mode 100644 index 0000000000..3f343ed841 --- /dev/null +++ b/app/serializers/similar_admin_user_serializer.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class SimilarAdminUserSerializer < AdminUserListSerializer + attributes :can_be_suspended, + :can_be_silenced + + def can_be_suspended + scope.can_suspend?(object) + end + + def can_be_silenced + scope.can_silence_user?(object) + end +end diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb index d460244b28..53c23bcf48 100644 --- a/app/serializers/site_serializer.rb +++ b/app/serializers/site_serializer.rb @@ -243,7 +243,7 @@ class SiteSerializer < ApplicationSerializer end def include_anonymous_default_sidebar_tags? - scope.anonymous? && SiteSetting.enable_experimental_sidebar_hamburger && SiteSetting.tagging_enabled && SiteSetting.default_sidebar_tags.present? + scope.anonymous? && !SiteSetting.legacy_navigation_menu? && SiteSetting.tagging_enabled && SiteSetting.default_sidebar_tags.present? end private diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index eb2db4e56e..0e9f975032 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -105,7 +105,7 @@ class UserSerializer < UserCardSerializer end def second_factor_enabled - object.totp_enabled? || object.security_keys_enabled? || object.backup_codes_enabled? + object.totp_enabled? || object.security_keys_enabled? end def include_second_factor_backup_enabled? diff --git a/app/serializers/web_hook_post_serializer.rb b/app/serializers/web_hook_post_serializer.rb index da7dd3bbd0..beaa673831 100644 --- a/app/serializers/web_hook_post_serializer.rb +++ b/app/serializers/web_hook_post_serializer.rb @@ -32,6 +32,7 @@ class WebHookPostSerializer < PostSerializer flair_bg_color flair_color notice + mentioned_users }.each do |attr| define_method("include_#{attr}?") do false diff --git a/app/services/category_hashtag_data_source.rb b/app/services/category_hashtag_data_source.rb index f2e7964d73..ab912bf1c4 100644 --- a/app/services/category_hashtag_data_source.rb +++ b/app/services/category_hashtag_data_source.rb @@ -8,9 +8,7 @@ class CategoryHashtagDataSource "folder" end - def self.category_to_hashtag_item(guardian_categories, category) - category = Category.new(category.slice(:id, :slug, :name, :parent_category_id, :description)) - + def self.category_to_hashtag_item(category) HashtagAutocompleteService::HashtagItem.new.tap do |item| item.text = category.name item.slug = category.slug @@ -22,9 +20,7 @@ class CategoryHashtagDataSource # categories here. item.ref = if category.parent_category_id - parent_category = - guardian_categories.find { |cat| cat[:id] === category.parent_category_id } - !parent_category ? category.slug : "#{parent_category[:slug]}:#{category.slug}" + "#{category.parent_category.slug}:#{category.slug}" else category.slug end @@ -32,31 +28,43 @@ class CategoryHashtagDataSource end def self.lookup(guardian, slugs) - # We use Site here because it caches all the categories the - # user has access to. - guardian_categories = Site.new(guardian).categories + user_categories = Category.secured(guardian).includes(:parent_category) Category - .query_from_cached_categories(slugs, guardian_categories) - .map { |category| category_to_hashtag_item(guardian_categories, category) } + .query_loaded_from_slugs(slugs, user_categories) + .map { |category| category_to_hashtag_item(category) } end def self.search(guardian, term, limit) - guardian_categories = Site.new(guardian).categories - - guardian_categories - .select do |category| - category[:name].downcase.include?(term) || category[:slug].downcase.include?(term) - end + Category + .secured(guardian) + .includes(:parent_category) + .where("LOWER(name) LIKE :term OR LOWER(slug) LIKE :term", term: "%#{term}%") .take(limit) - .map { |category| category_to_hashtag_item(guardian_categories, category) } + .map { |category| category_to_hashtag_item(category) } end def self.search_sort(search_results, term) - search_results - .select { |item| item.slug == term } - .sort_by { |item| item.text.downcase } - .concat( - search_results.select { |item| item.slug != term }.sort_by { |item| item.text.downcase }, + if term.present? + search_results.sort_by { |item| [item.slug == term ? 0 : 1, item.text.downcase] } + else + search_results.sort_by { |item| item.text.downcase } + end + end + + def self.search_without_term(guardian, limit) + Category + .includes(:parent_category) + .secured(guardian) + .joins( + "LEFT JOIN category_users ON category_users.user_id = #{guardian.user.id} + AND category_users.category_id = categories.id", ) + .where( + "category_users.notification_level IS NULL OR category_users.notification_level != ?", + CategoryUser.notification_levels[:muted], + ) + .order(topic_count: :desc) + .take(limit) + .map { |category| category_to_hashtag_item(category) } end end diff --git a/app/services/hashtag_autocomplete_service.rb b/app/services/hashtag_autocomplete_service.rb index 77fd2c62be..31ff7d1e09 100644 --- a/app/services/hashtag_autocomplete_service.rb +++ b/app/services/hashtag_autocomplete_service.rb @@ -122,7 +122,18 @@ class HashtagAutocompleteService # composer we want a slug without a suffix to be a category first, tag second. if slugs_without_suffixes.any? types_in_priority_order.each do |type| - found_from_slugs = execute_lookup!(lookup_results, type, guardian, slugs_without_suffixes) + # We do not want to continue fallback if there are conflicting slugs where + # one has a type and one does not, this may result in duplication. An + # example: + # + # A category with slug `management` is not found because of permissions + # and we also have a slug with suffix in the form of `management::tag`. + # There is a tag that exists with the `management` slug. The tag should + # not be found here but rather in the next lookup since it's got a more + # specific lookup with the type. + slugs_to_lookup = + slugs_without_suffixes.reject { |slug| slugs_with_suffixes.include?("#{slug}::#{type}") } + found_from_slugs = execute_lookup!(lookup_results, type, guardian, slugs_to_lookup) slugs_without_suffixes = slugs_without_suffixes - found_from_slugs.map(&:ref) break if slugs_without_suffixes.empty? @@ -139,6 +150,11 @@ class HashtagAutocompleteService .map { |slug| slug.gsub("::#{type}", "") } next if slugs_for_type.empty? execute_lookup!(lookup_results, type, guardian, slugs_for_type) + + # Make sure the refs are the same going out as they were going in. + lookup_results[type.to_sym].each do |item| + item.ref = "#{item.ref}::#{type}" if slugs_with_suffixes.include?("#{item.ref}::#{type}") + end end end @@ -172,6 +188,8 @@ class HashtagAutocompleteService raise Discourse::InvalidParameters.new(:order) if !types_in_priority_order.is_a?(Array) limit = [limit, SEARCH_MAX_LIMIT].min + return search_without_term(types_in_priority_order, limit) if term.blank? + limited_results = [] top_ranked_type = nil term = term.downcase @@ -279,6 +297,23 @@ class HashtagAutocompleteService private + def search_without_term(types_in_priority_order, limit) + split_limit = (limit.to_f / types_in_priority_order.length.to_f).ceil + limited_results = [] + + types_in_priority_order.each do |type| + search_results = @@data_sources[type].search_without_term(guardian, split_limit) + next if search_results.empty? + next if !all_data_items_valid?(search_results) + + # This is purposefully unsorted as search_without_term should sort + # in its own way. + limited_results.concat(set_types(set_refs(search_results), type)) + end + + limited_results.take(limit) + end + # Sometimes a specific ref is required, e.g. for categories that have # a parent their ref will be parent_slug:child_slug, though most of the # time it will be the same as the slug. The ref can then be used for @@ -287,12 +322,16 @@ class HashtagAutocompleteService hashtag_items.each { |item| item.ref ||= item.slug } end + def set_types(hashtag_items, type) + hashtag_items.each { |item| item.type = type } + end + def all_data_items_valid?(items) items.all? { |item| item.kind_of?(HashtagItem) && item.slug.present? && item.text.present? } end def search_for_type(type, guardian, term, limit) - set_refs(@@data_sources[type].search(guardian, term, limit)).each { |item| item.type = type } + set_types(set_refs(@@data_sources[type].search(guardian, term, limit)), type) end def execute_lookup!(lookup_results, type, guardian, slugs) @@ -308,6 +347,6 @@ class HashtagAutocompleteService end def lookup_for_type(type, guardian, slugs) - set_refs(@@data_sources[type].lookup(guardian, slugs)).each { |item| item.type = type } + set_types(set_refs(@@data_sources[type].lookup(guardian, slugs)), type) end end diff --git a/app/services/post_alerter.rb b/app/services/post_alerter.rb index 8c31d579ce..c4c930b5ca 100644 --- a/app/services/post_alerter.rb +++ b/app/services/post_alerter.rb @@ -126,6 +126,7 @@ class PostAlerter if mentioned_users mentioned_users = only_allowed_users(mentioned_users, post) + mentioned_users = mentioned_users - pm_watching_users(post) notified += notify_users(mentioned_users - notified, :mentioned, post, mentioned_opts) end @@ -642,6 +643,14 @@ class PostAlerter users end + def pm_watching_users(post) + return [] if !post.topic.private_message? + directly_targeted_users(post).filter do |u| + notification_level = TopicUser.get(post.topic, u)&.notification_level + notification_level == TopicUser.notification_levels[:watching] + end + end + def notify_pm_users(post, reply_to_user, quoted_users, notified) return [] unless post.topic @@ -660,8 +669,7 @@ class PostAlerter users = directly_targeted_users(post).reject { |u| notified.include?(u) } DiscourseEvent.trigger(:before_create_notifications_for_users, users, post) users.each do |user| - notification_level = TopicUser.get(post.topic, user)&.notification_level - if reply_to_user == user || notification_level == TopicUser.notification_levels[:watching] || user.staged? + if reply_to_user == user || pm_watching_users(post).include?(user) || user.staged? create_notification(user, Notification.types[:private_message], post, skip_send_email_to: emails_to_skip_send) end end diff --git a/app/services/tag_hashtag_data_source.rb b/app/services/tag_hashtag_data_source.rb index b761949dab..093e22c2c6 100644 --- a/app/services/tag_hashtag_data_source.rb +++ b/app/services/tag_hashtag_data_source.rb @@ -55,4 +55,23 @@ class TagHashtagDataSource def self.search_sort(search_results, _) search_results.sort_by { |result| result.text.downcase } end + + def self.search_without_term(guardian, limit) + return [] if !SiteSetting.tagging_enabled + + tags_with_counts, _ = + DiscourseTagging.filter_allowed_tags( + guardian, + with_context: true, + limit: limit, + for_input: true, + order_popularity: true, + excluded_tag_names: DiscourseTagging.muted_tags(guardian.user), + ) + + TagsController + .tag_counts_json(tags_with_counts) + .take(limit) + .map { |tag| tag_to_hashtag_item(tag, include_count: true) } + end end diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb index a614e3a326..81f7dafbab 100644 --- a/app/services/user_updater.rb +++ b/app/services/user_updater.rb @@ -180,8 +180,8 @@ class UserUpdater end end - if attributes.key?(:skip_new_user_tips) - user.user_option.seen_popups = user.user_option.skip_new_user_tips ? [-1] : nil + if attributes.key?(:skip_new_user_tips) && user.user_option.skip_new_user_tips + user.user_option.seen_popups = [-1] end # automatically disable digests when mailing_list_mode is enabled diff --git a/app/views/finish_installation/index.html.erb b/app/views/finish_installation/index.html.erb index de7ffeb009..c1e979e738 100644 --- a/app/views/finish_installation/index.html.erb +++ b/app/views/finish_installation/index.html.erb @@ -13,6 +13,6 @@
    - <%= image_tag "/images/wizard/tada.svg", class: "tada" %> + tada emoji
    diff --git a/app/views/invites/show.html.erb b/app/views/invites/show.html.erb index 08051aecfe..d9c30e4203 100644 --- a/app/views/invites/show.html.erb +++ b/app/views/invites/show.html.erb @@ -2,7 +2,7 @@ <%if flash[:error]%>
    - sweat-smile-face emoji + sweat-smile-face emoji
    <%=flash[:error].html_safe%> diff --git a/app/views/layouts/ember_cli.html.erb b/app/views/layouts/ember_cli.html.erb index bfc5649ecb..b6d953e1fb 100644 --- a/app/views/layouts/ember_cli.html.erb +++ b/app/views/layouts/ember_cli.html.erb @@ -18,6 +18,8 @@

    Then visit the following URL to use Discourse:

    http://<%= Discourse.current_hostname %>:4200<%= Discourse.base_path %>

    + +

    To disable this warning and allow direct Rails access, start the server with ALLOW_EMBER_CLI_PROXY_BYPASS=1

    diff --git a/app/views/list/list.erb b/app/views/list/list.erb index 63a9b91c0c..d6ea174963 100644 --- a/app/views/list/list.erb +++ b/app/views/list/list.erb @@ -57,14 +57,9 @@ <% @list.topics.each_with_index do |t,i| %> - - - <% if t.image_url.present? %> - - <% end %> - <%= t.title %> +