diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index ccadfb89777..0b3e2515d7d 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -10,7 +10,7 @@ permissions: jobs: triage: if: github.actor != 'discourse-translator-bot' - runs-on: debian-12 + runs-on: ubuntu-latest steps: - uses: actions/labeler@v5 diff --git a/.github/workflows/licenses.yml b/.github/workflows/licenses.yml index 9e7a6384beb..1b8252cb174 100644 --- a/.github/workflows/licenses.yml +++ b/.github/workflows/licenses.yml @@ -17,7 +17,7 @@ jobs: build: if: github.event_name == 'pull_request' || github.repository != 'discourse/discourse-private-mirror' name: run - runs-on: debian-12 + runs-on: ${{ (github.repository != 'discourse/discourse' && 'ubuntu-latest') || 'debian-12' }} container: discourse/discourse_test:slim timeout-minutes: 10 diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 13990395090..d016f50cd4a 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -17,7 +17,7 @@ jobs: build: if: github.event_name == 'pull_request' || github.repository != 'discourse/discourse-private-mirror' name: run - runs-on: debian-12 + runs-on: ${{ (github.repository != 'discourse/discourse' && 'ubuntu-latest') || 'debian-12' }} container: discourse/discourse_test:slim timeout-minutes: 30 diff --git a/.github/workflows/migration-tests.yml b/.github/workflows/migration-tests.yml index f39602de7eb..a1f4e119f8c 100644 --- a/.github/workflows/migration-tests.yml +++ b/.github/workflows/migration-tests.yml @@ -24,7 +24,7 @@ jobs: tests: if: github.event_name == 'pull_request' || github.repository != 'discourse/discourse-private-mirror' name: Tests with Ruby ${{ matrix.ruby }} - runs-on: debian-12 + runs-on: ${{ (github.repository != 'discourse/discourse' && 'ubuntu-latest') || 'debian-12' }} container: discourse/discourse_test:slim timeout-minutes: 20 @@ -126,11 +126,11 @@ jobs: if: steps.app-cache.outputs.cache-hit != 'true' run: rm -rf tmp/app-cache/uploads && cp -r public/uploads tmp/app-cache/uploads -# - name: Check core database drift -# run: | -# mkdir /tmp/intermediate_db -# ./migrations/scripts/schema_generator /tmp/intermediate_db/base_migration.sql -# diff -u migrations/common/intermediate_db_schema/000_base_schema.sql /tmp/intermediate_db/base_migration.sql + # - name: Check core database drift + # run: | + # mkdir /tmp/intermediate_db + # ./migrations/scripts/schema_generator /tmp/intermediate_db/base_migration.sql + # diff -u migrations/common/intermediate_db_schema/000_base_schema.sql /tmp/intermediate_db/base_migration.sql - name: RSpec run: bin/rspec --default-path migrations/spec diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml index 2d2fcf65a60..eb0a35d45a8 100644 --- a/.github/workflows/release-notes.yml +++ b/.github/workflows/release-notes.yml @@ -4,14 +4,14 @@ on: workflow_dispatch: inputs: from: - description: 'Starting ref (exclusive). Can be a tag, branch or commit ref. `latest-release` refers to the last beta version bump.' + description: "Starting ref (exclusive). Can be a tag, branch or commit ref. `latest-release` refers to the last beta version bump." required: true - default: 'latest-release' + default: "latest-release" type: string to: - description: 'Ending ref (inclusive). Can be a tag, branch or commit ref. `HEAD` refers to the most recent commit.' + description: "Ending ref (inclusive). Can be a tag, branch or commit ref. `HEAD` refers to the most recent commit." required: true - default: 'HEAD' + default: "HEAD" type: string permissions: @@ -20,7 +20,7 @@ permissions: jobs: build: name: run - runs-on: debian-12 + runs-on: ${{ (github.repository != 'discourse/discourse' && 'ubuntu-latest') || 'debian-12' }} container: discourse/discourse_test:slim timeout-minutes: 10 env: @@ -87,7 +87,7 @@ jobs: echo "From: $from_ref - $(git rev-parse --short $from_ref) - ${{ steps.dates.outputs.from }}" >> $GITHUB_STEP_SUMMARY echo "To: $to_ref - $(git rev-parse --short $to_ref) - ${{ steps.dates.outputs.to }}" >> $GITHUB_STEP_SUMMARY - + echo "" >> $GITHUB_STEP_SUMMARY echo "---" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/stale-pr-closer.yml b/.github/workflows/stale-pr-closer.yml index b0469f4f9ad..de7299191d9 100644 --- a/.github/workflows/stale-pr-closer.yml +++ b/.github/workflows/stale-pr-closer.yml @@ -1,7 +1,7 @@ -name: 'Close stale PRs' +name: "Close stale PRs" on: schedule: - - cron: '30 1 * * *' + - cron: "30 1 * * *" workflow_dispatch: permissions: @@ -9,11 +9,11 @@ permissions: jobs: stale: - runs-on: debian-12 + runs-on: ${{ (github.repository != 'discourse/discourse' && 'ubuntu-latest') || 'debian-12' }} steps: - uses: actions/stale@v9 with: days-before-stale: 60 days-before-close: 14 - stale-pr-message: 'This pull request has been automatically marked as stale because it has been open for 60 days with no activity. To keep it open, remove the stale tag, push code, or add a comment. Otherwise, it will be closed in 14 days.' + stale-pr-message: "This pull request has been automatically marked as stale because it has been open for 60 days with no activity. To keep it open, remove the stale tag, push code, or add a comment. Otherwise, it will be closed in 14 days." exempt-pr-labels: dependencies diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 80c4851c960..89737d8a00d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,7 +29,7 @@ jobs: build: if: github.event_name == 'pull_request' || github.repository != 'discourse/discourse-private-mirror' name: ${{ matrix.target }} ${{ matrix.build_type }} # Update fetch-job-id step if changing this - runs-on: ${{ ((github.ref == 'refs/heads/main') && 'debian-12') || 'ubuntu-22.04-8core' }} + runs-on: ${{ (github.repository != 'discourse/discourse' && 'ubuntu-22.04-8core') || 'debian-12' }} container: discourse/discourse_test:slim${{ (matrix.build_type == 'frontend' || matrix.build_type == 'system') && '-browsers' || '' }} timeout-minutes: 20 @@ -44,6 +44,7 @@ jobs: DISCOURSE_TURBO_RSPEC_RETRY_AND_LOG_FLAKY_TESTS: ${{ (matrix.build_type == 'system' || matrix.build_type == 'backend') && github.ref == 'refs/heads/main' && '1' }} CHEAP_SOURCE_MAPS: "1" TESTEM_DEFAULT_BROWSER: Chrome + MINIO_RUNNER_INSTALL_DIR: /home/discourse/.minio_runner strategy: fail-fast: false @@ -255,6 +256,13 @@ jobs: if: matrix.build_type == 'system' run: bin/ember-cli --build + - name: Minio cache + if: matrix.build_type == 'system' && matrix.target == 'core' + uses: actions/cache@v4 + with: + path: ${{ env.MINIO_RUNNER_INSTALL_DIR }} + key: ${{ runner.os }}-${{ steps.container-envs.outputs.ruby_version }}-${{ steps.container-envs.outputs.debian_release }}-gem-${{ hashFiles('**/Gemfile.lock') }} + - name: Ensure latest minio binary installed for Core System Tests if: matrix.build_type == 'system' && matrix.target == 'core' run: bundle exec ruby script/install_minio_binaries.rb @@ -358,7 +366,7 @@ jobs: core_frontend_tests: if: github.event_name == 'pull_request' || github.repository != 'discourse/discourse-private-mirror' name: core frontend (${{ matrix.browser }}) - runs-on: ${{ ((github.ref == 'refs/heads/main') && 'debian-12') || 'ubuntu-22.04-8core' }} + runs-on: ${{ (github.repository != 'discourse/discourse' && 'ubuntu-22.04-8core') || 'debian-12' }} container: image: discourse/discourse_test:slim-browsers options: --user discourse @@ -397,7 +405,7 @@ jobs: - name: Core QUnit working-directory: ./app/assets/javascripts/discourse run: | - pnpm ember exam --path /tmp/emberbuild --load-balance --parallel=$(($(nproc) / 2 + 1)) --launch "${{ env.TESTEM_BROWSER }}" --write-execution-file --random + pnpm ember exam --path /tmp/emberbuild --load-balance --parallel=$(($(nproc) / 2)) --launch "${{ env.TESTEM_BROWSER }}" --write-execution-file --random timeout-minutes: 15 - uses: actions/upload-artifact@v4 diff --git a/.licensed.yml b/.licensed.yml index cbd3d6472ab..1de021fc63f 100644 --- a/.licensed.yml +++ b/.licensed.yml @@ -45,6 +45,7 @@ reviewed: - concurrent-ruby # MIT - css_parser # MIT - drb # BSD-2-Clause + - dry-initializer # MIT - excon # MIT - faraday-em_http # MIT - faraday-em_synchrony # MIT diff --git a/Gemfile b/Gemfile index 81db1d4e4ae..0ebcced79e1 100644 --- a/Gemfile +++ b/Gemfile @@ -287,3 +287,5 @@ group :migrations, optional: true do # CLI gem "ruby-progressbar" end + +gem "dry-initializer", "~> 3.1" diff --git a/Gemfile.lock b/Gemfile.lock index 39988ad5f5a..4686a89a326 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -132,9 +132,10 @@ GEM literate_randomizer docile (1.4.1) drb (2.2.1) + dry-initializer (3.1.1) email_reply_trimmer (0.1.13) erubi (1.13.0) - excon (0.111.0) + excon (0.112.0) execjs (2.9.1) exifr (1.4.0) extralite-bundle (2.8.2) @@ -142,8 +143,9 @@ GEM faker (2.23.0) i18n (>= 1.8.11, < 2) fakeweb (1.3.0) - faraday (2.11.0) + faraday (2.12.0) faraday-net_http (>= 2.0, < 3.4) + json logger faraday-net_http (3.3.0) net-http @@ -158,16 +160,16 @@ GEM fspath (3.1.2) globalid (1.2.1) activesupport (>= 6.1) - google-protobuf (4.28.1-aarch64-linux) + google-protobuf (4.28.2-aarch64-linux) bigdecimal rake (>= 13) - google-protobuf (4.28.1-arm64-darwin) + google-protobuf (4.28.2-arm64-darwin) bigdecimal rake (>= 13) - google-protobuf (4.28.1-x86_64-darwin) + google-protobuf (4.28.2-x86_64-darwin) bigdecimal rake (>= 13) - google-protobuf (4.28.1-x86_64-linux) + google-protobuf (4.28.2-x86_64-linux) bigdecimal rake (>= 13) guess_html_encoding (0.0.11) @@ -178,7 +180,7 @@ GEM reline htmlentities (4.3.4) http_accept_language (2.1.1) - i18n (1.14.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) image_optim (0.31.3) exifr (~> 1.2, >= 1.2.2) @@ -189,7 +191,7 @@ GEM image_size (3.4.0) in_threads (1.6.0) io-console (0.7.2) - irb (1.14.0) + irb (1.14.1) rdoc (>= 4.0.0) reline (>= 0.4.2) iso8601 (0.13.0) @@ -234,7 +236,7 @@ GEM net-smtp matrix (0.4.2) maxminddb (0.1.22) - memory_profiler (1.0.2) + memory_profiler (1.1.0) message_bus (4.3.8) rack (>= 1.1.3) messageformat-wrapper (1.1.0) @@ -344,7 +346,7 @@ GEM psych (5.1.2) stringio public_suffix (6.0.1) - puma (6.4.2) + puma (6.4.3) nio4r (~> 2.0) racc (1.8.1) rack (2.2.9) @@ -404,10 +406,10 @@ GEM io-console (~> 0.5) request_store (1.7.0) rack (>= 1.4) - rexml (3.3.7) + rexml (3.3.8) rinku (2.0.6) rotp (6.3.0) - rouge (4.3.0) + rouge (4.4.0) rqrcode (2.2.0) chunky_png (~> 1.0) rqrcode_core (~> 1.0) @@ -426,7 +428,7 @@ GEM rspec-html-matchers (0.10.0) nokogiri (~> 1) rspec (>= 3.0.0.a) - rspec-mocks (3.13.1) + rspec-mocks (3.13.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-multi-mock (0.3.1) @@ -473,12 +475,12 @@ GEM rubocop-rspec_rails (>= 2.30.0) rubocop-factory_bot (2.26.1) rubocop (~> 1.61) - rubocop-rails (2.26.1) + rubocop-rails (2.26.2) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.52.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rspec (3.0.5) + rubocop-rspec (3.1.0) rubocop (~> 1.61) rubocop-rspec_rails (2.30.0) rubocop (~> 1.61) @@ -503,9 +505,9 @@ GEM google-protobuf (>= 3.25, < 5.0) sassc-embedded (1.77.7) sass-embedded (~> 1.77) - selenium-devtools (0.128.0) + selenium-devtools (0.129.0) selenium-webdriver (~> 4.2) - selenium-webdriver (4.24.0) + selenium-webdriver (4.25.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) @@ -527,7 +529,7 @@ GEM snaky_hash (2.0.1) hashie version_gem (~> 1.1, >= 1.1.1) - sprockets (3.7.4) + sprockets (3.7.5) base64 concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -535,10 +537,10 @@ GEM actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) - sqlite3 (2.0.4-aarch64-linux-gnu) - sqlite3 (2.0.4-arm64-darwin) - sqlite3 (2.0.4-x86_64-darwin) - sqlite3 (2.0.4-x86_64-linux-gnu) + sqlite3 (2.1.0-aarch64-linux-gnu) + sqlite3 (2.1.0-arm64-darwin) + sqlite3 (2.1.0-x86_64-darwin) + sqlite3 (2.1.0-x86_64-linux-gnu) sshkey (3.0.0) stackprof (0.2.26) stringio (3.1.1) @@ -553,10 +555,10 @@ GEM concurrent-ruby (~> 1.0) tzinfo-data (1.2024.2) tzinfo (>= 1.0.0) - uglifier (4.2.0) + uglifier (4.2.1) execjs (>= 0.3.0, < 3) unf (0.2.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) unicorn (6.1.0) kgio (~> 2.6) raindrops (~> 0.7) @@ -566,7 +568,7 @@ GEM web-push (3.0.1) jwt (~> 2.0) openssl (~> 3.0) - webmock (3.23.1) + webmock (3.24.0) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -623,6 +625,7 @@ DEPENDENCIES discourse-fonts discourse-seed-fu discourse_dev_assets + dry-initializer (~> 3.1) email_reply_trimmer excon execjs diff --git a/app/assets/javascripts/admin/addon/components/admin-config-area-cards/about/contact-information.gjs b/app/assets/javascripts/admin/addon/components/admin-config-area-cards/about/contact-information.gjs index db75ec52aa7..e0bf32ec63c 100644 --- a/app/assets/javascripts/admin/addon/components/admin-config-area-cards/about/contact-information.gjs +++ b/app/assets/javascripts/admin/addon/components/admin-config-area-cards/about/contact-information.gjs @@ -82,7 +82,7 @@ export default class AdminConfigAreasAboutContactInformation extends Component { @@ -96,7 +96,7 @@ export default class AdminConfigAreasAboutContactInformation extends Component { diff --git a/app/assets/javascripts/admin/addon/components/admin-config-area-cards/about/your-organization.gjs b/app/assets/javascripts/admin/addon/components/admin-config-area-cards/about/your-organization.gjs index 6a023333c48..3047874d07c 100644 --- a/app/assets/javascripts/admin/addon/components/admin-config-area-cards/about/your-organization.gjs +++ b/app/assets/javascripts/admin/addon/components/admin-config-area-cards/about/your-organization.gjs @@ -58,7 +58,7 @@ export default class AdminConfigAreasAboutYourOrganization extends Component { @@ -75,7 +75,7 @@ export default class AdminConfigAreasAboutYourOrganization extends Component { @@ -89,7 +89,7 @@ export default class AdminConfigAreasAboutYourOrganization extends Component { diff --git a/app/assets/javascripts/admin/addon/components/admin-filtered-site-settings.gjs b/app/assets/javascripts/admin/addon/components/admin-filtered-site-settings.gjs index 82f775bfb05..1ce46e36b0a 100644 --- a/app/assets/javascripts/admin/addon/components/admin-filtered-site-settings.gjs +++ b/app/assets/javascripts/admin/addon/components/admin-filtered-site-settings.gjs @@ -61,7 +61,7 @@ export default class AdminFilteredSiteSettings extends Component { /> -
+
{{#each this.visibleSettings as |setting|}} {{/each}} diff --git a/app/assets/javascripts/admin/addon/components/admin-notice.gjs b/app/assets/javascripts/admin/addon/components/admin-notice.gjs new file mode 100644 index 00000000000..07138307c7d --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/admin-notice.gjs @@ -0,0 +1,25 @@ +import Component from "@glimmer/component"; +import { action } from "@ember/object"; +import { htmlSafe } from "@ember/template"; +import DButton from "discourse/components/d-button"; +import icon from "discourse-common/helpers/d-icon"; + +export default class AdminNotice extends Component { + @action + dismiss() { + this.args.dismissCallback(this.args.problem); + } + + +} diff --git a/app/assets/javascripts/admin/addon/components/admin-plugin-config-page.gjs b/app/assets/javascripts/admin/addon/components/admin-plugin-config-page.gjs index 262101958d5..17f9b477587 100644 --- a/app/assets/javascripts/admin/addon/components/admin-plugin-config-page.gjs +++ b/app/assets/javascripts/admin/addon/components/admin-plugin-config-page.gjs @@ -24,6 +24,10 @@ export default class AdminPluginConfigPage extends Component { return classes.join(" "); } + get actionsOutletName() { + return `admin-plugin-config-page-actions-${this.args.plugin.kebabCaseName}`; + } + linkText(navLink) { if (navLink.label) { return i18n(navLink.label); @@ -68,10 +72,12 @@ export default class AdminPluginConfigPage extends Component { {{/if}} <:actions as |actions|> - +
+ +
diff --git a/app/assets/javascripts/admin/addon/components/admin-section-landing-item.gjs b/app/assets/javascripts/admin/addon/components/admin-section-landing-item.gjs new file mode 100644 index 00000000000..71071b2e7fd --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/admin-section-landing-item.gjs @@ -0,0 +1,77 @@ +import Component from "@glimmer/component"; +import { hash } from "@ember/helper"; +import { LinkTo } from "@ember/routing"; +import concatClass from "discourse/helpers/concat-class"; +import dIcon from "discourse-common/helpers/d-icon"; +import i18n from "discourse-common/helpers/i18n"; +import { + DangerButton, + DefaultButton, + PrimaryButton, +} from "admin/components/admin-page-action-button"; + +export default class AdminSectionLandingItem extends Component { + get title() { + if (this.args.titleLabelTranslated) { + return this.args.titleLabelTranslated; + } else if (this.args.titleLabel) { + return i18n(this.args.titleLabel); + } + } + + get description() { + if (this.args.descriptionLabelTranslated) { + return this.args.descriptionLabelTranslated; + } else if (this.args.descriptionLabel) { + return i18n(this.args.descriptionLabel); + } + } + + get tagline() { + if (this.args.taglineLabelTranslated) { + return this.args.taglineLabelTranslated; + } else if (this.args.taglineLabel) { + return i18n(this.args.taglineLabel); + } + } + + +} diff --git a/app/assets/javascripts/admin/addon/components/admin-section-landing-wrapper.gjs b/app/assets/javascripts/admin/addon/components/admin-section-landing-wrapper.gjs new file mode 100644 index 00000000000..12396698ba8 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/admin-section-landing-wrapper.gjs @@ -0,0 +1,7 @@ +const AdminSectionLandingWrapper = ; + +export default AdminSectionLandingWrapper; diff --git a/app/assets/javascripts/admin/addon/components/dashboard-problems.gjs b/app/assets/javascripts/admin/addon/components/dashboard-problems.gjs new file mode 100644 index 00000000000..882d65501d6 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/dashboard-problems.gjs @@ -0,0 +1,78 @@ +import Component from "@glimmer/component"; +import { concat } from "@ember/helper"; +import { action } from "@ember/object"; +import { eq } from "truth-helpers"; +import ConditionalLoadingSection from "discourse/components/conditional-loading-section"; +import DButton from "discourse/components/d-button"; +import concatClass from "discourse/helpers/concat-class"; +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import icon from "discourse-common/helpers/d-icon"; +import i18n from "discourse-common/helpers/i18n"; +import AdminNotice from "admin/components/admin-notice"; + +export default class DashboardProblems extends Component { + @action + async dismissProblem(problem) { + try { + await ajax(`/admin/admin_notices/${problem.id}`, { type: "DELETE" }); + this.args.problems.removeObject(problem); + } catch (error) { + popupAjaxError(error); + } + } + + get problems() { + return this.args.problems.sortBy("priority"); + } + + +} diff --git a/app/assets/javascripts/admin/addon/components/dashboard-problems.hbs b/app/assets/javascripts/admin/addon/components/dashboard-problems.hbs deleted file mode 100644 index 10bbf1ed75f..00000000000 --- a/app/assets/javascripts/admin/addon/components/dashboard-problems.hbs +++ /dev/null @@ -1,58 +0,0 @@ -{{#if this.foundProblems}} -
-
-

- {{d-icon "heart"}} - {{i18n "admin.dashboard.problems_found"}} -

-
- -
- - {{#if this.highPriorityProblems.length}} -
-
    - {{#each this.highPriorityProblems as |problem|}} -
  • - {{d-icon "triangle-exclamation"}} - {{html-safe problem.message}} -
  • - {{/each}} -
-
- {{/if}} - -
-
    - {{#each this.lowPriorityProblems as |problem|}} -
  • {{html-safe problem.message}}
  • - {{/each}} -
-
- -

- - {{i18n "admin.dashboard.last_checked"}}: - {{this.problemsTimestamp}} -

-
-
-
-{{/if}} \ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/components/dashboard-problems.js b/app/assets/javascripts/admin/addon/components/dashboard-problems.js deleted file mode 100644 index c4df8401e0f..00000000000 --- a/app/assets/javascripts/admin/addon/components/dashboard-problems.js +++ /dev/null @@ -1,3 +0,0 @@ -import Component from "@ember/component"; - -export default class DashboardProblems extends Component {} diff --git a/app/assets/javascripts/admin/addon/components/highlighted-code.gjs b/app/assets/javascripts/admin/addon/components/highlighted-code.gjs new file mode 100644 index 00000000000..935e8c76a7d --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/highlighted-code.gjs @@ -0,0 +1,25 @@ +import Component from "@glimmer/component"; +import { service } from "@ember/service"; +import { modifier } from "ember-modifier"; +import highlightSyntax from "discourse/lib/highlight-syntax"; + +export default class HighlightedCode extends Component { + @service session; + @service siteSettings; + + highlight = modifier(async (element) => { + const code = document.createElement("code"); + code.classList.add(`lang-${this.args.lang}`); + code.textContent = this.args.code; + + const pre = document.createElement("pre"); + pre.appendChild(code); + + element.replaceChildren(pre); + await highlightSyntax(pre, this.siteSettings, this.session); + }); + + +} diff --git a/app/assets/javascripts/admin/addon/components/highlighted-code.hbs b/app/assets/javascripts/admin/addon/components/highlighted-code.hbs deleted file mode 100644 index bbe184349e4..00000000000 --- a/app/assets/javascripts/admin/addon/components/highlighted-code.hbs +++ /dev/null @@ -1 +0,0 @@ -
{{this.code}}
\ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/components/highlighted-code.js b/app/assets/javascripts/admin/addon/components/highlighted-code.js deleted file mode 100644 index 65208dc6eff..00000000000 --- a/app/assets/javascripts/admin/addon/components/highlighted-code.js +++ /dev/null @@ -1,11 +0,0 @@ -import Component from "@ember/component"; -import { observes, on } from "@ember-decorators/object"; -import highlightSyntax from "discourse/lib/highlight-syntax"; - -export default class HighlightedCode extends Component { - @on("didInsertElement") - @observes("code") - _refresh() { - highlightSyntax(this.element, this.siteSettings, this.session); - } -} diff --git a/app/assets/javascripts/admin/addon/controllers/admin-dashboard.js b/app/assets/javascripts/admin/addon/controllers/admin-dashboard.js index 89a6a52ad19..8f60607eba4 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-dashboard.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-dashboard.js @@ -18,16 +18,6 @@ export default class AdminDashboardController extends Controller { @setting("version_checks") showVersionChecks; - @discourseComputed( - "lowPriorityProblems.length", - "highPriorityProblems.length" - ) - foundProblems(lowPriorityProblemsLength, highPriorityProblemsLength) { - const problemsLength = - lowPriorityProblemsLength + highPriorityProblemsLength; - return this.currentUser.admin && problemsLength > 0; - } - @computed("siteSettings.dashboard_visible_tabs") get visibleTabs() { return (this.siteSettings.dashboard_visible_tabs || "") @@ -106,16 +96,7 @@ export default class AdminDashboardController extends Controller { }); AdminDashboard.fetchProblems() - .then((model) => { - this.set( - "highPriorityProblems", - model.problems.filterBy("priority", "high") - ); - this.set( - "lowPriorityProblems", - model.problems.filterBy("priority", "low") - ); - }) + .then((model) => this.set("problems", model.problems)) .finally(() => this.set("loadingProblems", false)); } diff --git a/app/assets/javascripts/admin/addon/controllers/admin-user-index.js b/app/assets/javascripts/admin/addon/controllers/admin-user-index.js index b3133a9f053..d1fac2a80d3 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-user-index.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-user-index.js @@ -78,8 +78,8 @@ export default class AdminUserIndexController extends Controller.extend( @discourseComputed("model.associated_accounts") associatedAccounts(associatedAccounts) { return associatedAccounts - .map((provider) => `${provider.name} (${provider.description})`) - .join(", "); + ?.map((provider) => `${provider.name} (${provider.description})`) + ?.join(", "); } @discourseComputed("model.user_fields.[]") @@ -319,6 +319,16 @@ export default class AdminUserIndexController extends Controller.extend( return this.model.silence(); } + @action + deleteAssociatedAccounts() { + this.dialog.yesNoConfirm({ + message: I18n.t("admin.user.delete_associated_accounts_confirm"), + didConfirm: () => { + this.model.deleteAssociatedAccounts().catch(popupAjaxError); + }, + }); + } + @action anonymize() { const user = this.model; diff --git a/app/assets/javascripts/admin/addon/models/admin-plugin.js b/app/assets/javascripts/admin/addon/models/admin-plugin.js index 33730888da8..02d940d9470 100644 --- a/app/assets/javascripts/admin/addon/models/admin-plugin.js +++ b/app/assets/javascripts/admin/addon/models/admin-plugin.js @@ -24,6 +24,10 @@ export default class AdminPlugin { return this.name.replaceAll("-", "_"); } + get kebabCaseName() { + return this.name.replaceAll(" ", "-").replaceAll("_", "-"); + } + get translatedCategoryName() { // We do this because the site setting list is grouped by category, // with plugins that have their root site setting key defined as `plugins:` diff --git a/app/assets/javascripts/admin/addon/models/admin-user.js b/app/assets/javascripts/admin/addon/models/admin-user.js index 79489398db6..0449fd5056c 100644 --- a/app/assets/javascripts/admin/addon/models/admin-user.js +++ b/app/assets/javascripts/admin/addon/models/admin-user.js @@ -287,6 +287,17 @@ export default class AdminUser extends User { }); } + deleteAssociatedAccounts() { + return ajax(`/admin/users/${this.id}/delete_associated_accounts`, { + type: "PUT", + data: { + context: window.location.pathname, + }, + }).then(() => { + this.set("associated_accounts", []); + }); + } + destroy(formData) { return ajax(`/admin/users/${this.id}.json`, { type: "DELETE", 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 c0adae5c180..facec60a4f4 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-route-map.js +++ b/app/assets/javascripts/admin/addon/routes/admin-route-map.js @@ -239,5 +239,13 @@ export default function () { path: "/whats-new", resetNamespace: true, }); + + this.route( + "adminSection", + { path: "/section", resetNamespace: true }, + function () { + this.route("account"); + } + ); }); } diff --git a/app/assets/javascripts/admin/addon/templates/dashboard.hbs b/app/assets/javascripts/admin/addon/templates/dashboard.hbs index 2d0e81ff674..f5a07025ee1 100644 --- a/app/assets/javascripts/admin/addon/templates/dashboard.hbs +++ b/app/assets/javascripts/admin/addon/templates/dashboard.hbs @@ -18,9 +18,7 @@ diff --git a/app/assets/javascripts/admin/addon/templates/section-account.hbs b/app/assets/javascripts/admin/addon/templates/section-account.hbs new file mode 100644 index 00000000000..3c50c9114e9 --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/section-account.hbs @@ -0,0 +1,26 @@ + + <:breadcrumbs> + + + + + + + + \ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/templates/user-index.hbs b/app/assets/javascripts/admin/addon/templates/user-index.hbs index a2c69d3864f..6244c0f7022 100644 --- a/app/assets/javascripts/admin/addon/templates/user-index.hbs +++ b/app/assets/javascripts/admin/addon/templates/user-index.hbs @@ -157,6 +157,17 @@ /> {{/if}} + {{#if (and this.currentUser.admin this.associatedAccounts)}} +
+ +
+ {{/if}} {{/if}} diff --git a/app/assets/javascripts/admin/package.json b/app/assets/javascripts/admin/package.json index 4e20f9d3a5b..3feeeace71b 100644 --- a/app/assets/javascripts/admin/package.json +++ b/app/assets/javascripts/admin/package.json @@ -14,32 +14,32 @@ "start": "ember serve" }, "dependencies": { - "@babel/core": "^7.25.2", + "@babel/core": "^7.25.7", "@ember/string": "^4.0.0", "discourse-common": "workspace:1.0.0", "ember-cli-babel": "^8.2.0", "ember-cli-htmlbars": "^6.3.0", - "ember-template-imports": "^4.1.1" + "ember-template-imports": "^4.1.2" }, "devDependencies": { "@ember/optional-features": "^2.1.0", "@embroider/test-setup": "^4.0.0", "@glimmer/component": "^1.1.2", - "@types/jquery": "^3.5.30", + "@types/jquery": "^3.5.31", "@types/qunit": "^2.19.10", "@types/rsvp": "^4.0.9", "broccoli-asset-rev": "^3.0.0", - "ember-cli": "~5.11.0", + "ember-cli": "~5.12.0", "ember-cli-inject-live-reload": "^2.1.0", "ember-cli-sri": "^2.1.1", "ember-cli-terser": "^4.0.2", "ember-disable-prototype-extensions": "^1.1.3", - "ember-load-initializers": "^2.1.1", - "ember-resolver": "^13.0.0", + "ember-load-initializers": "^3.0.1", + "ember-resolver": "^13.0.2", "ember-source": "~5.5.0", "ember-source-channel-url": "^3.0.0", "loader.js": "^4.7.0", - "webpack": "^5.94.0" + "webpack": "^5.95.0" }, "engines": { "node": ">= 18", diff --git a/app/assets/javascripts/dialog-holder/package.json b/app/assets/javascripts/dialog-holder/package.json index e4c917ba997..db76fd4f93a 100644 --- a/app/assets/javascripts/dialog-holder/package.json +++ b/app/assets/javascripts/dialog-holder/package.json @@ -9,17 +9,17 @@ ], "dependencies": { "a11y-dialog": "8.1.1", - "ember-auto-import": "^2.7.4", + "ember-auto-import": "^2.8.1", "ember-cli-babel": "^8.2.0", "ember-cli-htmlbars": "^6.3.0", - "ember-template-imports": "^4.1.1", + "ember-template-imports": "^4.1.2", "truth-helpers": "workspace:1.0.0" }, "devDependencies": { - "@types/jquery": "^3.5.30", + "@types/jquery": "^3.5.31", "@types/qunit": "^2.19.10", "@types/rsvp": "^4.0.9", - "webpack": "^5.94.0" + "webpack": "^5.95.0" }, "engines": { "node": ">= 18", diff --git a/app/assets/javascripts/discourse-common/addon/lib/raw-handlebars-helpers.js b/app/assets/javascripts/discourse-common/addon/lib/raw-handlebars-helpers.js index df1164c3e3a..c9550c038a3 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/raw-handlebars-helpers.js +++ b/app/assets/javascripts/discourse-common/addon/lib/raw-handlebars-helpers.js @@ -44,11 +44,12 @@ export function registerRawHelpers(hbs, handlebarsClass, owner) { } let list = get(this, contextName); let output = []; - let innerContext = { ...options.contexts[0] }; + let innerContext = options.contexts[0]; for (let i = 0; i < list.length; i++) { innerContext[localName] = list[i]; output.push(options.fn(innerContext)); } + delete innerContext[localName]; return output.join(""); } ); diff --git a/app/assets/javascripts/discourse-common/package.json b/app/assets/javascripts/discourse-common/package.json index eaca3fbde7e..c350a4c64a4 100644 --- a/app/assets/javascripts/discourse-common/package.json +++ b/app/assets/javascripts/discourse-common/package.json @@ -14,7 +14,7 @@ "start": "ember serve" }, "dependencies": { - "@babel/core": "^7.25.2", + "@babel/core": "^7.25.7", "@ember/string": "^4.0.0", "@uppy/aws-s3": "3.0.6", "@uppy/aws-s3-multipart": "3.1.3", @@ -23,10 +23,10 @@ "@uppy/utils": "5.4.3", "@uppy/xhr-upload": "3.1.1", "discourse-i18n": "workspace:1.0.0", - "ember-auto-import": "^2.7.4", + "ember-auto-import": "^2.8.1", "ember-cli-babel": "^8.2.0", "ember-cli-htmlbars": "^6.3.0", - "ember-resolver": "^13.0.0", + "ember-resolver": "^13.0.2", "handlebars": "^4.7.8", "truth-helpers": "workspace:1.0.0" }, @@ -34,20 +34,20 @@ "@ember/optional-features": "^2.1.0", "@embroider/test-setup": "^4.0.0", "@glimmer/component": "^1.1.2", - "@types/jquery": "^3.5.30", + "@types/jquery": "^3.5.31", "@types/qunit": "^2.19.10", "@types/rsvp": "^4.0.9", "broccoli-asset-rev": "^3.0.0", - "ember-cli": "~5.11.0", + "ember-cli": "~5.12.0", "ember-cli-inject-live-reload": "^2.1.0", "ember-cli-sri": "^2.1.1", "ember-cli-terser": "^4.0.2", "ember-disable-prototype-extensions": "^1.1.3", - "ember-load-initializers": "^2.1.1", + "ember-load-initializers": "^3.0.1", "ember-source": "~5.5.0", "ember-source-channel-url": "^3.0.0", "loader.js": "^4.7.0", - "webpack": "^5.94.0" + "webpack": "^5.95.0" }, "engines": { "node": ">= 18", diff --git a/app/assets/javascripts/discourse-markdown-it/package.json b/app/assets/javascripts/discourse-markdown-it/package.json index e5814b85e9c..e674e89183a 100644 --- a/app/assets/javascripts/discourse-markdown-it/package.json +++ b/app/assets/javascripts/discourse-markdown-it/package.json @@ -21,7 +21,7 @@ "@embroider/addon-shim": "^1.8.9", "discourse-common": "workspace:1.0.0", "discourse-i18n": "workspace:1.0.0", - "ember-auto-import": "^2.7.4", + "ember-auto-import": "^2.8.1", "markdown-it": "14.0.0", "pretty-text": "workspace:1.0.0", "truth-helpers": "workspace:1.0.0", diff --git a/app/assets/javascripts/discourse-plugins/package.json b/app/assets/javascripts/discourse-plugins/package.json index 8e8360d71a0..0f20f77df4e 100644 --- a/app/assets/javascripts/discourse-plugins/package.json +++ b/app/assets/javascripts/discourse-plugins/package.json @@ -8,18 +8,18 @@ "ember-addon" ], "dependencies": { - "@babel/core": "^7.25.2", + "@babel/core": "^7.25.7", "deprecation-silencer": "workspace:1.0.0", "discourse-hbr": "workspace:1.0.0", "discourse-widget-hbs": "workspace:1.0.0", "ember-cli-babel": "^8.2.0", "ember-cli-htmlbars": "^6.3.0", - "ember-template-imports": "^4.1.1", + "ember-template-imports": "^4.1.2", "ember-this-fallback": "^0.4.0" }, "devDependencies": { - "ember-cli": "~5.11.0", - "webpack": "^5.94.0" + "ember-cli": "~5.12.0", + "webpack": "^5.95.0" }, "engines": { "node": ">= 18", diff --git a/app/assets/javascripts/discourse-widget-hbs/package.json b/app/assets/javascripts/discourse-widget-hbs/package.json index 2ada9b8bb40..c983e4ccc99 100644 --- a/app/assets/javascripts/discourse-widget-hbs/package.json +++ b/app/assets/javascripts/discourse-widget-hbs/package.json @@ -14,8 +14,8 @@ "start": "ember serve" }, "dependencies": { - "@babel/core": "^7.25.2", - "ember-auto-import": "^2.7.4", + "@babel/core": "^7.25.7", + "ember-auto-import": "^2.8.1", "ember-cli-babel": "^8.2.0", "ember-cli-htmlbars": "^6.3.0", "handlebars": "^4.7.8" @@ -26,17 +26,17 @@ "@glimmer/component": "^1.1.2", "@glimmer/syntax": "^0.92.3", "broccoli-asset-rev": "^3.0.0", - "ember-cli": "~5.11.0", + "ember-cli": "~5.12.0", "ember-cli-inject-live-reload": "^2.1.0", "ember-cli-sri": "^2.1.1", "ember-cli-terser": "^4.0.2", "ember-disable-prototype-extensions": "^1.1.3", - "ember-load-initializers": "^2.1.1", - "ember-resolver": "^13.0.0", + "ember-load-initializers": "^3.0.1", + "ember-resolver": "^13.0.2", "ember-source": "~5.5.0", "ember-source-channel-url": "^3.0.0", "loader.js": "^4.7.0", - "webpack": "^5.94.0" + "webpack": "^5.95.0" }, "engines": { "node": ">= 18", diff --git a/app/assets/javascripts/discourse/app/components/ace-editor.gjs b/app/assets/javascripts/discourse/app/components/ace-editor.gjs index 213ffda552e..01c49e3db9c 100644 --- a/app/assets/javascripts/discourse/app/components/ace-editor.gjs +++ b/app/assets/javascripts/discourse/app/components/ace-editor.gjs @@ -3,7 +3,7 @@ import { tracked } from "@glimmer/tracking"; import didInsert from "@ember/render-modifiers/modifiers/did-insert"; import didUpdate from "@ember/render-modifiers/modifiers/did-update"; import { service } from "@ember/service"; -import { registerWaiter } from "@ember/test"; +import { buildWaiter } from "@ember/test-waiters"; import { modifier } from "ember-modifier"; import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner"; import loadAce from "discourse/lib/load-ace-editor"; @@ -11,6 +11,7 @@ import { isTesting } from "discourse-common/config/environment"; import { bind } from "discourse-common/utils/decorators"; import I18n from "discourse-i18n"; +const WAITER = buildWaiter("ace-editor"); const COLOR_VARS_REGEX = /\$(primary|secondary|tertiary|quaternary|header_background|header_primary|highlight|danger|success|love)(\s|;|-(low|medium|high))/g; @@ -72,11 +73,10 @@ export default class AceEditor extends Component { this.editor.getSession().setValue(this.args.content || ""); this.skipChangePropagation = false; - if (isTesting()) { - let finished = false; - registerWaiter(() => finished); - this.editor.renderer.once("afterRender", () => (finished = true)); - } + const token = WAITER.beginAsync(); + this.editor.renderer.once("afterRender", () => WAITER.endAsync(token)); + + return () => WAITER.endAsync(token); }); constructor() { @@ -94,7 +94,7 @@ export default class AceEditor extends Component { this.appEvents.on("ace:resize", this.resize); window.addEventListener("resize", this.resize); this._darkModeListener = window.matchMedia("(prefers-color-scheme: dark)"); - this._darkModeListener.addListener(this.setAceTheme); + this._darkModeListener.addEventListener("change", this.setAceTheme); } willDestroy() { @@ -102,7 +102,7 @@ export default class AceEditor extends Component { this.editor?.destroy(); - this._darkModeListener?.removeListener(this.setAceTheme); + this._darkModeListener?.removeEventListener("change", this.setAceTheme); window.removeEventListener("resize", this.resize); this.appEvents.off("ace:resize", this.resize); } diff --git a/app/assets/javascripts/discourse/app/components/categories-boxes.hbs b/app/assets/javascripts/discourse/app/components/categories-boxes.hbs index d5469e87332..949f0740e1a 100644 --- a/app/assets/javascripts/discourse/app/components/categories-boxes.hbs +++ b/app/assets/javascripts/discourse/app/components/categories-boxes.hbs @@ -1,100 +1,110 @@ -{{#each this.categories as |c|}} - - -
-
- {{#unless c.isMuted}} - - {{/unless}} - -
- + + {{#each this.categories as |c|}} + +
+
{{#unless c.isMuted}} -
- {{html-safe c.description_excerpt}} + + {{/unless}} + +
+ - {{#if c.isGrandParent}} - {{#each c.subcategories as |subcategory|}} -
-
- - {{#if subcategory.subcategories}} -
- {{#each subcategory.subcategories as |subsubcategory|}} - {{#unless subsubcategory.isMuted}} - - - {{category-link subsubcategory hideParent="true"}} - - {{/unless}} - {{/each}} -
- {{/if}} -
-
- {{/each}} - {{else if c.subcategories}} -
- {{#each c.subcategories as |sc|}} - - - {{#if sc.uploaded_logo.url}} - - {{/if}} - - - {{category-link sc hideParent="true"}} - - {{/each}} + {{#unless c.isMuted}} +
+ {{html-safe c.description_excerpt}}
- {{/if}} - {{/unless}} + + {{#if c.isGrandParent}} + {{#each c.subcategories as |subcategory|}} +
+
+ + {{#if subcategory.subcategories}} +
+ {{#each subcategory.subcategories as |subsubcategory|}} + {{#unless subsubcategory.isMuted}} + + + {{category-link subsubcategory hideParent="true"}} + + {{/unless}} + {{/each}} +
+ {{/if}} +
+
+ {{/each}} + {{else if c.subcategories}} + + {{/if}} + {{/unless}} +
+ +
- -
-
- -{{/each}} + + {{/each}} + - - - {{i18n "categories.category"}} - {{i18n "categories.topics"}} - {{#if this.showTopics}} - {{i18n "categories.latest"}} - {{/if}} - - - - {{#each this.categories as |category|}} - - {{/each}} - - - {{/if}} - - {{#if this.mutedCategories}} -
- -

{{i18n "categories.muted"}}

- {{#if this.mutedToggleIcon}} - {{d-icon this.mutedToggleIcon}} - {{/if}} -
- + + {{#if this.categories}} + {{#if this.filteredCategories}} +
{{#if this.showTopics}} @@ -51,19 +18,61 @@ {{/if}} - + {{#each this.categories as |category|}} {{/each}}
{{i18n "categories.category"}} {{i18n "categories.topics"}}
-
+ {{/if}} + + {{#if this.mutedCategories}} +
+ +

{{i18n "categories.muted"}}

+ {{#if this.mutedToggleIcon}} + {{d-icon this.mutedToggleIcon}} + {{/if}} +
+ + + + + + {{#if this.showTopics}} + + {{/if}} + + + + {{#each this.categories as |category|}} + + {{/each}} + +
{{i18n "categories.category"}}{{i18n "categories.topics"}}{{i18n "categories.latest"}}
+
+ {{/if}} {{/if}} -{{/if}} +
- {{#unless this.category.isUncategorizedCategory}} + +
+ {{#unless this.category.isUncategorizedCategory}} +
+ + +
+ {{/unless}}
- +
- {{/unless}} -
- -
-
\ No newline at end of file +
\ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/glimmer-site-header.gjs b/app/assets/javascripts/discourse/app/components/glimmer-site-header.gjs index 690dde544a8..4997d3e8c3f 100644 --- a/app/assets/javascripts/discourse/app/components/glimmer-site-header.gjs +++ b/app/assets/javascripts/discourse/app/components/glimmer-site-header.gjs @@ -222,9 +222,7 @@ export default class GlimmerSiteHeader extends Component { } ).finished; - if (isTesting()) { - waitForPromise(animationFinished); - } + waitForPromise(animationFinished); cloakElement.animate([{ opacity: 0 }], { fill: "forwards" }); cloakElement.style.display = "block"; diff --git a/app/assets/javascripts/discourse/app/components/modal/json-schema-editor.js b/app/assets/javascripts/discourse/app/components/modal/json-schema-editor.js index ebdb2d22839..e3c69fb0e69 100644 --- a/app/assets/javascripts/discourse/app/components/modal/json-schema-editor.js +++ b/app/assets/javascripts/discourse/app/components/modal/json-schema-editor.js @@ -3,7 +3,6 @@ import { tracked } from "@glimmer/tracking"; import { action } from "@ember/object"; import { waitForPromise } from "@ember/test-waiters"; import { create } from "virtual-dom"; -import { isTesting } from "discourse-common/config/environment"; import { iconNode } from "discourse-common/lib/icon-library"; export default class JsonSchemaEditorModal extends Component { @@ -38,9 +37,7 @@ export default class JsonSchemaEditorModal extends Component { @action async buildJsonEditor(element) { const promise = import("@json-editor/json-editor"); - if (isTesting()) { - waitForPromise(promise); - } + waitForPromise(promise); const { JSONEditor } = await promise; JSONEditor.defaults.options.theme = "barebones"; diff --git a/app/assets/javascripts/discourse/app/components/topic-admin-menu.gjs b/app/assets/javascripts/discourse/app/components/topic-admin-menu.gjs index 4899bc4e6c8..0e5cfef0717 100644 --- a/app/assets/javascripts/discourse/app/components/topic-admin-menu.gjs +++ b/app/assets/javascripts/discourse/app/components/topic-admin-menu.gjs @@ -77,7 +77,8 @@ export default class TopicAdminMenu extends Component { return ( this.currentUser?.canManageTopic || this.details?.can_archive_topic || - this.details?.can_close_topic + this.details?.can_close_topic || + this.details?.can_split_merge_topic ); } diff --git a/app/assets/javascripts/discourse/app/components/topic-list/activity-column.gjs b/app/assets/javascripts/discourse/app/components/topic-list/activity-column.gjs index 3f9875433b2..9b522af40ee 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list/activity-column.gjs +++ b/app/assets/javascripts/discourse/app/components/topic-list/activity-column.gjs @@ -1,39 +1,29 @@ -import Component from "@glimmer/component"; import { hash } from "@ember/helper"; -import { service } from "@ember/service"; import { htmlSafe } from "@ember/template"; import PluginOutlet from "discourse/components/plugin-outlet"; import coldAgeClass from "discourse/helpers/cold-age-class"; import concatClass from "discourse/helpers/concat-class"; -import element from "discourse/helpers/element"; import formatDate from "discourse/helpers/format-date"; -export default class ActivityColumn extends Component { - @service siteSettings; - - get wrapperElement() { - return element(this.args.tagName ?? "td"); - } - - ; +export default ActivityColumn; diff --git a/app/assets/javascripts/discourse/app/components/topic-list/featured-topic.gjs b/app/assets/javascripts/discourse/app/components/topic-list/featured-topic.gjs index 0f80da98f6b..d11fce1be2d 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list/featured-topic.gjs +++ b/app/assets/javascripts/discourse/app/components/topic-list/featured-topic.gjs @@ -1,6 +1,5 @@ import { on } from "@ember/modifier"; import { htmlSafe } from "@ember/template"; -import TopicEntrance from "discourse/components/topic-list/topic-entrance"; import TopicPostBadges from "discourse/components/topic-post-badges"; import TopicStatus from "discourse/components/topic-status"; import formatAge from "discourse/helpers/format-age"; @@ -30,13 +29,11 @@ const FeaturedTopic = ; diff --git a/app/assets/javascripts/discourse/app/components/topic-list/latest-topic-list-item.gjs b/app/assets/javascripts/discourse/app/components/topic-list/latest-topic-list-item.gjs index c940bd0a958..510397eb637 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list/latest-topic-list-item.gjs +++ b/app/assets/javascripts/discourse/app/components/topic-list/latest-topic-list-item.gjs @@ -1,6 +1,5 @@ import Component from "@glimmer/component"; import { concat, hash } from "@ember/helper"; -import { service } from "@ember/service"; import PluginOutlet from "discourse/components/plugin-outlet"; import PostsCountColumn from "discourse/components/topic-list/posts-count-column"; import TopicPostBadges from "discourse/components/topic-post-badges"; @@ -16,12 +15,8 @@ import topicFeaturedLink from "discourse/helpers/topic-featured-link"; import topicLink from "discourse/helpers/topic-link"; export default class LatestTopicListItem extends Component { - @service appEvents; - get tagClassNames() { - if (this.args.topic.tags) { - return this.args.topic.tags.map((tagName) => `tag-${tagName}`); - } + return this.args.topic.tags?.map((tagName) => `tag-${tagName}`); } } diff --git a/app/assets/javascripts/discourse/app/components/topic-list/topic-cell.gjs b/app/assets/javascripts/discourse/app/components/topic-list/topic-cell.gjs new file mode 100644 index 00000000000..583cf3b0f6f --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/topic-list/topic-cell.gjs @@ -0,0 +1,168 @@ +import Component from "@glimmer/component"; +import { hash } from "@ember/helper"; +import { on } from "@ember/modifier"; +import { action } from "@ember/object"; +import { service } from "@ember/service"; +import PluginOutlet from "discourse/components/plugin-outlet"; +import ActionList from "discourse/components/topic-list/action-list"; +import ParticipantGroups from "discourse/components/topic-list/participant-groups"; +import TopicExcerpt from "discourse/components/topic-list/topic-excerpt"; +import TopicLink from "discourse/components/topic-list/topic-link"; +import UnreadIndicator from "discourse/components/topic-list/unread-indicator"; +import TopicPostBadges from "discourse/components/topic-post-badges"; +import TopicStatus from "discourse/components/topic-status"; +import categoryLink from "discourse/helpers/category-link"; +import discourseTags from "discourse/helpers/discourse-tags"; +import topicFeaturedLink from "discourse/helpers/topic-featured-link"; +import { groupPath } from "discourse/lib/url"; +import { bind } from "discourse-common/utils/decorators"; +import I18n from "discourse-i18n"; + +export default class TopicCell extends Component { + @service currentUser; + @service messageBus; + + constructor() { + super(...arguments); + + if (this.includeUnreadIndicator) { + this.messageBus.subscribe(this.unreadIndicatorChannel, this.onMessage); + } + } + + willDestroy() { + super.willDestroy(...arguments); + + this.messageBus.unsubscribe(this.unreadIndicatorChannel, this.onMessage); + } + + @bind + onMessage(data) { + const nodeClassList = document.querySelector( + `.indicator-topic-${data.topic_id}` + ).classList; + + nodeClassList.toggle("read", !data.show_indicator); + } + + get unreadIndicatorChannel() { + return `/private-messages/unread-indicator/${this.args.topic.id}`; + } + + get includeUnreadIndicator() { + return typeof this.args.topic.unread_by_group_member !== "undefined"; + } + + get unreadClass() { + return this.args.topic.unread_by_group_member ? "" : "read"; + } + + get newDotText() { + return this.currentUser?.trust_level > 0 + ? "" + : I18n.t("filters.new.lower_title"); + } + + get participantGroups() { + if (!this.args.topic.participant_groups) { + return []; + } + + return this.args.topic.participant_groups.map((name) => ({ + name, + url: groupPath(name), + })); + } + + @action + onTitleFocus(event) { + event.target.classList.add("selected"); + } + + @action + onTitleBlur(event) { + event.target.classList.remove("selected"); + } + + +} diff --git a/app/assets/javascripts/discourse/app/components/topic-list/topic-entrance.gjs b/app/assets/javascripts/discourse/app/components/topic-list/topic-entrance.gjs deleted file mode 100644 index 329dc4153e4..00000000000 --- a/app/assets/javascripts/discourse/app/components/topic-list/topic-entrance.gjs +++ /dev/null @@ -1,111 +0,0 @@ -import Component from "@glimmer/component"; -import { fn } from "@ember/helper"; -import { on } from "@ember/modifier"; -import { action } from "@ember/object"; -import { service } from "@ember/service"; -import { htmlSafe } from "@ember/template"; -import DiscourseURL from "discourse/lib/url"; -import icon from "discourse-common/helpers/d-icon"; -import i18n from "discourse-common/helpers/i18n"; -import I18n from "discourse-i18n"; -import DMenu from "float-kit/components/d-menu"; - -function entranceDate(dt, showTime) { - const today = new Date(); - - if (dt.toDateString() === today.toDateString()) { - return moment(dt).format(I18n.t("dates.time")); - } - - if (dt.getYear() === today.getYear()) { - // No year - return moment(dt).format( - showTime - ? I18n.t("dates.long_date_without_year_with_linebreak") - : I18n.t("dates.long_no_year_no_time") - ); - } - - return moment(dt).format( - showTime - ? I18n.t("dates.long_date_with_year_with_linebreak") - : I18n.t("dates.long_date_with_year_without_time") - ); -} - -export default class TopicEntrance extends Component { - @service historyStore; - - get createdDate() { - return new Date(this.args.topic.created_at); - } - - get bumpedDate() { - return new Date(this.args.topic.bumped_at); - } - - get showTime() { - return ( - this.bumpedDate.getTime() - this.createdDate.getTime() < - 1000 * 60 * 60 * 24 * 2 - ); - } - - get topDate() { - return entranceDate(this.createdDate, this.showTime); - } - - get bottomDate() { - return entranceDate(this.bumpedDate, this.showTime); - } - - @action - jumpTo(destination) { - this.historyStore.set("lastTopicIdViewed", this.args.topic.id); - DiscourseURL.routeTo(destination); - } - - -} diff --git a/app/assets/javascripts/discourse/app/components/topic-list/topic-list-header-column.gjs b/app/assets/javascripts/discourse/app/components/topic-list/topic-list-header-column.gjs index 3bdeaf02dad..41b3ef7798a 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list/topic-list-header-column.gjs +++ b/app/assets/javascripts/discourse/app/components/topic-list/topic-list-header-column.gjs @@ -9,7 +9,6 @@ import icon from "discourse-common/helpers/d-icon"; import i18n from "discourse-common/helpers/i18n"; export default class TopicListHeaderColumn extends Component { - @service modal; @service router; get localizedName() { diff --git a/app/assets/javascripts/discourse/app/components/topic-list/topic-list-item.gjs b/app/assets/javascripts/discourse/app/components/topic-list/topic-list-item.gjs index d0a77f9d5be..c140ca2d685 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list/topic-list-item.gjs +++ b/app/assets/javascripts/discourse/app/components/topic-list/topic-list-item.gjs @@ -8,16 +8,13 @@ import { service } from "@ember/service"; import { modifier } from "ember-modifier"; import { eq, gt } from "truth-helpers"; import PluginOutlet from "discourse/components/plugin-outlet"; -import ActionList from "discourse/components/topic-list/action-list"; import ActivityColumn from "discourse/components/topic-list/activity-column"; -import ParticipantGroups from "discourse/components/topic-list/participant-groups"; import PostCountOrBadges from "discourse/components/topic-list/post-count-or-badges"; import PostersColumn from "discourse/components/topic-list/posters-column"; import PostsCountColumn from "discourse/components/topic-list/posts-count-column"; +import TopicCell from "discourse/components/topic-list/topic-cell"; import TopicExcerpt from "discourse/components/topic-list/topic-excerpt"; import TopicLink from "discourse/components/topic-list/topic-link"; -import UnreadIndicator from "discourse/components/topic-list/unread-indicator"; -import TopicPostBadges from "discourse/components/topic-post-badges"; import TopicStatus from "discourse/components/topic-status"; import { topicTitleDecorators } from "discourse/components/topic-title"; import avatar from "discourse/helpers/avatar"; @@ -28,18 +25,12 @@ import formatDate from "discourse/helpers/format-date"; import number from "discourse/helpers/number"; import topicFeaturedLink from "discourse/helpers/topic-featured-link"; import { wantsNewWindow } from "discourse/lib/intercept-click"; -import DiscourseURL, { groupPath } from "discourse/lib/url"; +import DiscourseURL from "discourse/lib/url"; import icon from "discourse-common/helpers/d-icon"; import i18n from "discourse-common/helpers/i18n"; -import { bind } from "discourse-common/utils/decorators"; -import I18n from "discourse-i18n"; export default class TopicListItem extends Component { - @service appEvents; - @service currentUser; @service historyStore; - @service messageBus; - @service router; @service site; @service siteSettings; @@ -60,58 +51,10 @@ export default class TopicListItem extends Component { } }); - constructor() { - super(...arguments); - - if (this.includeUnreadIndicator) { - this.messageBus.subscribe(this.unreadIndicatorChannel, this.onMessage); - } - } - - willDestroy() { - super.willDestroy(...arguments); - - this.messageBus.unsubscribe(this.unreadIndicatorChannel, this.onMessage); - } - - @bind - onMessage(data) { - const nodeClassList = document.querySelector( - `.indicator-topic-${data.topic_id}` - ).classList; - - nodeClassList.toggle("read", !data.show_indicator); - } - - get unreadIndicatorChannel() { - return `/private-messages/unread-indicator/${this.args.topic.id}`; - } - - get includeUnreadIndicator() { - return typeof this.args.topic.unread_by_group_member !== "undefined"; - } - get isSelected() { return this.args.selected?.includes(this.args.topic); } - get participantGroups() { - if (!this.args.topic.participant_groups) { - return []; - } - - return this.args.topic.participant_groups.map((name) => ({ - name, - url: groupPath(name), - })); - } - - get newDotText() { - return this.currentUser?.trust_level > 0 - ? "" - : I18n.t("filters.new.lower_title"); - } - get tagClassNames() { return this.args.topic.tags?.map((tagName) => `tag-${tagName}`); } @@ -135,10 +78,6 @@ export default class TopicListItem extends Component { return this.site.desktopView && this.args.focusLastVisitedTopic; } - get unreadClass() { - return this.args.topic.unread_by_group_member ? "" : "read"; - } - navigateToTopic(topic, href) { this.historyStore.set("lastTopicIdViewed", topic.id); DiscourseURL.routeTo(href || topic.url); @@ -154,16 +93,6 @@ export default class TopicListItem extends Component { element.classList.add("highlighted"); } - @action - onTitleFocus(event) { - event.target.classList.add("selected"); - } - - @action - onTitleBlur(event) { - event.target.classList.remove("selected"); - } - @action applyTitleDecorators(element) { const rawTopicLink = element.querySelector(".raw-topic-link"); @@ -300,85 +229,13 @@ export default class TopicListItem extends Component { {{/if}} - - - - - {{~! no whitespace ~}} - - {{~! no whitespace ~}} - - {{~! no whitespace ~}} - - {{~#if @topic.featured_link~}} -   - {{~topicFeaturedLink @topic}} - {{~/if~}} - - {{~! no whitespace ~}} - - {{~#if @showTopicPostBadges~}} - - {{~/if~}} - - - - - {{#if this.expandPinned}} - - {{/if}} - - - + {{/if}} - {{#if @field.subtitle}} - {{@field.subtitle}} + {{#if @field.description}} + {{@field.description}} {{/if}}
- +
diff --git a/app/assets/javascripts/discourse/app/form-kit/components/fk/control/radio-group.gjs b/app/assets/javascripts/discourse/app/form-kit/components/fk/control/radio-group.gjs index d5431746481..634904f032f 100644 --- a/app/assets/javascripts/discourse/app/form-kit/components/fk/control/radio-group.gjs +++ b/app/assets/javascripts/discourse/app/form-kit/components/fk/control/radio-group.gjs @@ -3,7 +3,6 @@ import { hash } from "@ember/helper"; import FKFieldset from "discourse/form-kit/components/fk/fieldset"; import FKControlRadioGroupRadio from "./radio-group/radio"; -// eslint-disable-next-line ember/no-empty-glimmer-component-classes export default class FKControlRadioGroup extends Component { static controlType = "radio-group"; diff --git a/app/assets/javascripts/discourse/app/form-kit/components/fk/field.gjs b/app/assets/javascripts/discourse/app/form-kit/components/fk/field.gjs index 9a1c3f04e97..ac66e6b85e9 100644 --- a/app/assets/javascripts/discourse/app/form-kit/components/fk/field.gjs +++ b/app/assets/javascripts/discourse/app/form-kit/components/fk/field.gjs @@ -49,7 +49,6 @@ export default class FKField extends Component { this.field = this.args.registerField(this.name, { triggerRevalidationFor: this.args.triggerRevalidationFor, title: this.args.title, - subtitle: this.args.subtitle, description: this.args.description, showTitle: this.args.showTitle, collectionIndex: this.args.collectionIndex, diff --git a/app/assets/javascripts/discourse/app/form-kit/components/fk/meta.gjs b/app/assets/javascripts/discourse/app/form-kit/components/fk/meta.gjs index 9adcf4b5e3c..799a5439a4f 100644 --- a/app/assets/javascripts/discourse/app/form-kit/components/fk/meta.gjs +++ b/app/assets/javascripts/discourse/app/form-kit/components/fk/meta.gjs @@ -1,7 +1,6 @@ import Component from "@glimmer/component"; import FKCharCounter from "discourse/form-kit/components/fk/char-counter"; import FKErrors from "discourse/form-kit/components/fk/errors"; -import FKText from "discourse/form-kit/components/fk/text"; export default class FKMeta extends Component { get shouldRenderCharCounter() { @@ -9,12 +8,7 @@ export default class FKMeta extends Component { } get shouldRenderMeta() { - return ( - this.showMeta && - (this.shouldRenderCharCounter || - this.args.error || - this.args.description?.length) - ); + return this.showMeta && (this.shouldRenderCharCounter || this.args.error); } get showMeta() { @@ -26,8 +20,6 @@ export default class FKMeta extends Component {
{{#if @error}} - {{else if @description}} - {{@description}} {{/if}} {{#if this.shouldRenderCharCounter}} diff --git a/app/assets/javascripts/discourse/app/instance-initializers/hashtag-css-generator.js b/app/assets/javascripts/discourse/app/instance-initializers/hashtag-css-generator.js index 7bfef4fe959..36c013e73f5 100644 --- a/app/assets/javascripts/discourse/app/instance-initializers/hashtag-css-generator.js +++ b/app/assets/javascripts/discourse/app/instance-initializers/hashtag-css-generator.js @@ -12,11 +12,8 @@ export default { * with the hashtag type via api.registerHashtagType. The default * ones in core are CategoryHashtagType and TagHashtagType. */ - initialize(owner) { - this.site = owner.lookup("service:site"); - + initialize() { const cssTag = document.createElement("style"); - cssTag.type = "text/css"; cssTag.id = "hashtag-css-generator"; cssTag.innerHTML = Object.values(getHashtagTypeClasses()) .map((hashtagType) => hashtagType.generatePreloadedCssClasses()) diff --git a/app/assets/javascripts/discourse/app/instance-initializers/register-media-optimization-upload-processor.js b/app/assets/javascripts/discourse/app/instance-initializers/register-media-optimization-upload-processor.js index 0ab944add76..732a1f784ce 100644 --- a/app/assets/javascripts/discourse/app/instance-initializers/register-media-optimization-upload-processor.js +++ b/app/assets/javascripts/discourse/app/instance-initializers/register-media-optimization-upload-processor.js @@ -8,14 +8,6 @@ export default { const capabilities = owner.lookup("service:capabilities"); if (siteSettings.composer_media_optimization_image_enabled) { - // NOTE: There are various performance issues with the Canvas - // in iOS Safari that are causing crashes when processing images - // with spikes of over 100% CPU usage. The cause of this is unknown, - // but profiling points to CanvasRenderingContext2D.getImageData() - // and CanvasRenderingContext2D.drawImage(). - // - // Until Safari makes some progress with OffscreenCanvas or other - // alternatives we cannot support this workflow. if ( capabilities.isIOS && !siteSettings.composer_ios_media_optimisation_image_enabled @@ -23,6 +15,22 @@ export default { return; } + // Restrict feature to browsers that support OffscreenCanvas + if (typeof OffscreenCanvas === "undefined") { + return; + } + if (!("createImageBitmap" in self)) { + return; + } + + // prior to v18, Safari has WASM memory growth bugs + // eg https://github.com/emscripten-core/emscripten/issues/19144 + let match = window.navigator.userAgent.match(/Mobile\/([0-9]+)\./); + let safariVersion = match ? parseInt(match[1], 10) : null; + if (capabilities.isSafari && safariVersion && safariVersion < 18) { + return; + } + addComposerUploadPreProcessor( UppyMediaOptimization, ({ isMobileDevice }) => { diff --git a/app/assets/javascripts/discourse/app/instance-initializers/sharing-sources.js b/app/assets/javascripts/discourse/app/instance-initializers/sharing-sources.js index 587f8ab134a..7b83c5401ae 100644 --- a/app/assets/javascripts/discourse/app/instance-initializers/sharing-sources.js +++ b/app/assets/javascripts/discourse/app/instance-initializers/sharing-sources.js @@ -7,10 +7,10 @@ export default { Sharing.addSource({ id: "twitter", - icon: "fab-twitter", + icon: "fab-x-twitter", generateUrl(link, title, quote = "") { const text = quote ? `"${quote}" -- ` : title; - return `http://twitter.com/intent/tweet?url=${encodeURIComponent( + return `http://x.com/intent/tweet?url=${encodeURIComponent( link )}&text=${encodeURIComponent(text)}`; }, diff --git a/app/assets/javascripts/discourse/app/instance-initializers/webview-background.js b/app/assets/javascripts/discourse/app/instance-initializers/webview-background.js index 0e68eab1e31..07bdae4780f 100644 --- a/app/assets/javascripts/discourse/app/instance-initializers/webview-background.js +++ b/app/assets/javascripts/discourse/app/instance-initializers/webview-background.js @@ -4,23 +4,39 @@ import discourseLater from "discourse-common/lib/later"; // Send bg color to webview so iOS status bar matches site theme export default { after: "inject-objects", + retryCount: 0, initialize(owner) { const caps = owner.lookup("service:capabilities"); if (caps.isAppWebview) { window .matchMedia("(prefers-color-scheme: dark)") - .addListener(this.updateAppBackground); + .addEventListener("change", this.updateAppBackground); this.updateAppBackground(); } }, - updateAppBackground() { + + updateAppBackground(delay = 500) { discourseLater(() => { - const header = document.querySelector(".d-header-wrap .d-header"); - if (header) { - const styles = window.getComputedStyle(header); - postRNWebviewMessage("headerBg", styles.backgroundColor); + if (this.headerBgColor()) { + postRNWebviewMessage("headerBg", this.headerBgColor()); + } else { + this.retry(); } - }, 500); + }, delay); + }, + + headerBgColor() { + const header = document.querySelector(".d-header-wrap .d-header"); + if (header) { + return window.getComputedStyle(header)?.backgroundColor; + } + }, + + retry() { + if (this.retryCount < 2) { + this.retryCount++; + this.updateAppBackground(1000); + } }, }; diff --git a/app/assets/javascripts/discourse/app/lib/after-frame-paint.js b/app/assets/javascripts/discourse/app/lib/after-frame-paint.js index aa1cdb4eba2..62065c4809d 100644 --- a/app/assets/javascripts/discourse/app/lib/after-frame-paint.js +++ b/app/assets/javascripts/discourse/app/lib/after-frame-paint.js @@ -1,17 +1,13 @@ -import DEBUG from "@glimmer/env"; -import { registerWaiter } from "@ember/test"; -import { isTesting } from "discourse-common/config/environment"; +import { buildWaiter } from "@ember/test-waiters"; + +const WAITER = buildWaiter("after-frame-paint"); /** * Runs `callback` shortly after the next browser Frame is produced. * ref: https://webperf.tips/tip/measuring-paint-time */ export default function runAfterFramePaint(callback) { - let done = false; - - if (DEBUG && isTesting()) { - registerWaiter(() => done); - } + const token = WAITER.beginAsync(); // Queue a "before Render Steps" callback via requestAnimationFrame. requestAnimationFrame(() => { @@ -21,7 +17,7 @@ export default function runAfterFramePaint(callback) { // Setup the callback to run in a Task messageChannel.port1.onmessage = () => { - done = true; + WAITER.endAsync(token); callback(); }; diff --git a/app/assets/javascripts/discourse/app/lib/load-ace-editor.js b/app/assets/javascripts/discourse/app/lib/load-ace-editor.js index 09a4c3ca253..8899470d5f1 100644 --- a/app/assets/javascripts/discourse/app/lib/load-ace-editor.js +++ b/app/assets/javascripts/discourse/app/lib/load-ace-editor.js @@ -1,10 +1,7 @@ import { waitForPromise } from "@ember/test-waiters"; -import { isTesting } from "discourse-common/config/environment"; export default async function loadAce() { const promise = import("discourse/static/ace-editor-bundle"); - if (isTesting()) { - waitForPromise(promise); - } + waitForPromise(promise); return await promise; } diff --git a/app/assets/javascripts/discourse/app/lib/load-script.js b/app/assets/javascripts/discourse/app/lib/load-script.js index 840c705e8c5..1c517704ba9 100644 --- a/app/assets/javascripts/discourse/app/lib/load-script.js +++ b/app/assets/javascripts/discourse/app/lib/load-script.js @@ -1,32 +1,28 @@ import { run } from "@ember/runloop"; -import { registerWaiter } from "@ember/test"; +import { buildWaiter } from "@ember/test-waiters"; import { Promise } from "rsvp"; import { ajax } from "discourse/lib/ajax"; import { PUBLIC_JS_VERSIONS } from "discourse/lib/public-js-versions"; -import { isTesting } from "discourse-common/config/environment"; import getURL, { getURLWithCDN } from "discourse-common/lib/get-url"; +const WAITER = buildWaiter("load-script"); const _loaded = {}; const _loading = {}; function loadWithTag(path, cb) { const head = document.getElementsByTagName("head")[0]; - let finished = false; let s = document.createElement("script"); s.src = path; - if (isTesting()) { - registerWaiter(() => finished); - } + const token = WAITER.beginAsync(); // Don't leave it hanging if something goes wrong s.onerror = function () { - finished = true; + WAITER.endAsync(token); }; s.onload = s.onreadystatechange = function (_, abort) { - finished = true; if ( abort || !s.readyState || @@ -38,6 +34,8 @@ function loadWithTag(path, cb) { run(null, cb); } } + + WAITER.endAsync(token); }; head.appendChild(s); diff --git a/app/assets/javascripts/discourse/app/lib/media-optimization-utils.js b/app/assets/javascripts/discourse/app/lib/media-optimization-utils.js index 1e087cebe89..1050a81f861 100644 --- a/app/assets/javascripts/discourse/app/lib/media-optimization-utils.js +++ b/app/assets/javascripts/discourse/app/lib/media-optimization-utils.js @@ -1,35 +1,5 @@ -import { Promise } from "rsvp"; -import { helperContext } from "discourse-common/lib/helpers"; - -// Chrome and Firefox use a native method to do Image -> Bitmap Array (it happens of the main thread!) -// Safari < 15 uses the `` element due to https://bugs.webkit.org/show_bug.cgi?id=182424 -// Safari > 15 still uses `` due to their buggy createImageBitmap not handling EXIF rotation async function fileToDrawable(file) { - const caps = helperContext().capabilities; - - if ("createImageBitmap" in self && !caps.isApple) { - return await createImageBitmap(file); - } else { - const url = URL.createObjectURL(file); - const img = new Image(); - img.decoding = "async"; - img.src = url; - const loaded = new Promise((resolve, reject) => { - img.onload = () => resolve(); - img.onerror = () => reject(Error("Image loading error")); - }); - - if (img.decode) { - // Nice off-thread way supported in Safari/Chrome. - // Safari throws on decode if the source is SVG. - // https://bugs.webkit.org/show_bug.cgi?id=188347 - await img.decode().catch(() => null); - } - - // Always await loaded, as we may have bailed due to the Safari bug above. - await loaded; - return img; - } + return await createImageBitmap(file); } function drawableToImageData(drawable) { @@ -40,17 +10,7 @@ function drawableToImageData(drawable) { sw = width, sh = height; - const offscreenCanvasSupported = typeof OffscreenCanvas !== "undefined"; - - // Make canvas same size as image - let canvas; - if (offscreenCanvasSupported) { - canvas = new OffscreenCanvas(width, height); - } else { - canvas = document.createElement("canvas"); - canvas.width = width; - canvas.height = height; - } + let canvas = new OffscreenCanvas(width, height); // Draw image onto canvas const ctx = canvas.getContext("2d"); @@ -60,10 +20,6 @@ function drawableToImageData(drawable) { ctx.drawImage(drawable, sx, sy, sw, sh, 0, 0, width, height); const imageData = ctx.getImageData(0, 0, width, height); - if (!offscreenCanvasSupported) { - canvas.remove(); - } - return imageData; } diff --git a/app/assets/javascripts/discourse/app/lib/optional-service.js b/app/assets/javascripts/discourse/app/lib/optional-service.js index 07412b59c1b..f57bdfc0b0f 100644 --- a/app/assets/javascripts/discourse/app/lib/optional-service.js +++ b/app/assets/javascripts/discourse/app/lib/optional-service.js @@ -2,8 +2,16 @@ import { computed } from "@ember/object"; import { getOwner } from "@ember/owner"; import { dasherize } from "@ember/string"; -export default function (name) { - return computed(function (defaultName) { +export default function (target, name, descriptor) { + name ??= target; + + const decorator = computed(function (defaultName) { return getOwner(this).lookup(`service:${name || dasherize(defaultName)}`); }); + + if (descriptor) { + return decorator(target, name, descriptor); + } else { + return decorator; + } } diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.gjs b/app/assets/javascripts/discourse/app/lib/plugin-api.gjs index 1cc160fd546..77310b8cdbb 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.gjs +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.gjs @@ -3,7 +3,7 @@ // docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version // using the format described at https://keepachangelog.com/en/1.0.0/. -export const PLUGIN_API_VERSION = "1.37.1"; +export const PLUGIN_API_VERSION = "1.37.2"; import $ from "jquery"; import { h } from "virtual-dom"; @@ -2646,7 +2646,7 @@ class PluginApi { /** * Changes the lock icon used for a sidebar category section link to indicate that a category is read restricted. * - * @param {String} Name of a FontAwesome 5 icon + * @param {String} Name of a FontAwesome icon */ registerCustomCategorySectionLinkLockIcon(icon) { return registerCustomCategoryLockIcon(icon); @@ -2670,7 +2670,7 @@ class PluginApi { * @param {string} arg.categoryId - The id of the category * @param {string} arg.prefixType - The type of prefix to use. Can be "icon", "image", "text" or "span". * @param {string} arg.prefixValue - The value of the prefix to use. - * For "icon", pass in the name of a FontAwesome 5 icon. + * For "icon", pass in the name of a FontAwesome icon. * For "image", pass in the src of the image. * For "text", pass in the text to display. * For "span", pass in an array containing two hex color values. Example: `[FF0000, 000000]`. @@ -2706,7 +2706,7 @@ class PluginApi { * * @param {Object} arg - An object * @param {string} arg.tagName - The name of the tag - * @param {string} arg.prefixValue - The name of a FontAwesome 5 icon. + * @param {string} arg.prefixValue - The name of a FontAwesome icon. * @param {string} arg.prefixColor - The color represented using hexadecimal to use for the prefix. Example: "#FF0000" or "#FFF". */ registerCustomTagSectionLinkPrefixIcon({ @@ -3002,7 +3002,7 @@ class PluginApi { * return class extends UserMenuTab { * id = "custom-tab-id"; * panelComponent = MyCustomPanelGlimmerComponent; - * icon = "some-fa5-icon"; + * icon = "some-fa-icon"; * * get shouldDisplay() { * return this.siteSettings.enable_custom_tab && this.currentUser.admin; diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/custom-community-section-links.js b/app/assets/javascripts/discourse/app/lib/sidebar/custom-community-section-links.js index 38be61a3f06..40e14006362 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/custom-community-section-links.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/custom-community-section-links.js @@ -16,7 +16,7 @@ export let secondaryCustomSectionLinks = []; * @param {string} [args.route] - The Ember route name to generate the href attribute for the link. * @param {string} [args.href] - The href attribute for the link. * @param {string} [args.title] - The title attribute for the link. - * @param {string} [args.icon] - The FontAwesome 5 icon to display for the link. + * @param {string} [args.icon] - The FontAwesome icon to display for the link. * @param {Boolean} [secondary] - Determines whether the section link should be added to the main or secondary section in the "More..." links drawer. */ export function addSectionLink(args, secondary) { diff --git a/app/assets/javascripts/discourse/app/raw-templates/mobile/list/topic-list-item.hbr b/app/assets/javascripts/discourse/app/raw-templates/mobile/list/topic-list-item.hbr index 8c56a7f6b99..19d1a79986e 100644 --- a/app/assets/javascripts/discourse/app/raw-templates/mobile/list/topic-list-item.hbr +++ b/app/assets/javascripts/discourse/app/raw-templates/mobile/list/topic-list-item.hbr @@ -58,4 +58,5 @@
+ {{~raw-plugin-outlet name="topic-list-after-columns"}} diff --git a/app/assets/javascripts/discourse/app/services/media-optimization-worker.js b/app/assets/javascripts/discourse/app/services/media-optimization-worker.js index 8debb67bb37..f39ecd03dc7 100644 --- a/app/assets/javascripts/discourse/app/services/media-optimization-worker.js +++ b/app/assets/javascripts/discourse/app/services/media-optimization-worker.js @@ -29,6 +29,8 @@ export default class MediaOptimizationWorkerService extends Service { workerUrl = getAbsoluteURL("/javascripts/media-optimization-worker.js"); currentComposerUploadData = null; promiseResolvers = null; + workerDoneCount = 0; + workerPendingCount = 0; async optimizeImage(data, opts = {}) { this.promiseResolvers = this.promiseResolvers || {}; @@ -98,6 +100,7 @@ export default class MediaOptimizationWorkerService extends Service { }, [imageData.data.buffer] ); + this.workerPendingCount++; }); } @@ -147,7 +150,9 @@ export default class MediaOptimizationWorkerService extends Service { this.workerInstalled = false; this.worker.terminate(); this.worker = null; + this.workerDoneCount = 0; } + this.workerPendingCount = 0; } registerMessageHandler() { @@ -163,6 +168,13 @@ export default class MediaOptimizationWorkerService extends Service { this.promiseResolvers[e.data.fileId](optimizedFile); + this.workerDoneCount++; + this.workerPendingCount--; + if (this.workerDoneCount > 4 && this.workerPendingCount === 0) { + this.logIfDebug("Terminating worker to release memory in WASM."); + this.stopWorker(); + } + break; case "error": this.logIfDebug( @@ -174,6 +186,7 @@ export default class MediaOptimizationWorkerService extends Service { } this.promiseResolvers[e.data.fileId](); + this.workerPendingCount--; break; case "installed": this.logIfDebug("Worker installed."); diff --git a/app/assets/javascripts/discourse/app/templates/user/badges.hbs b/app/assets/javascripts/discourse/app/templates/user/badges.hbs index 1fceef5f5a6..c24c32ad1a3 100644 --- a/app/assets/javascripts/discourse/app/templates/user/badges.hbs +++ b/app/assets/javascripts/discourse/app/templates/user/badges.hbs @@ -1,32 +1,43 @@ {{body-class "user-badges-page"}}
- {{#if this.siteSettings.max_favorite_badges}} -

- {{i18n - "badges.favorite_count" - count=this.favoriteBadges.length - max=this.siteSettings.max_favorite_badges - }} -

- {{/if}} + + {{#if this.siteSettings.max_favorite_badges}} +

+ {{i18n + "badges.favorite_count" + count=this.favoriteBadges.length + max=this.siteSettings.max_favorite_badges + }} +

+ {{/if}} -
- {{#each this.sortedBadges as |ub|}} - + {{#each this.sortedBadges as |ub|}} + + {{/each}} + - {{/each}} - -
+
+
\ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/widgets/button.js b/app/assets/javascripts/discourse/app/widgets/button.js index a863ceb180f..831b0087a7f 100644 --- a/app/assets/javascripts/discourse/app/widgets/button.js +++ b/app/assets/javascripts/discourse/app/widgets/button.js @@ -62,6 +62,10 @@ export const ButtonClass = { attributes["aria-pressed"] = attrs.ariaPressed; } + if (attrs.ariaLive) { + attributes["aria-live"] = attrs.ariaLive; + } + if (attrs.tabAttrs) { const tab = attrs.tabAttrs; attributes["aria-selected"] = tab["aria-selected"]; diff --git a/app/assets/javascripts/discourse/app/widgets/post-menu.js b/app/assets/javascripts/discourse/app/widgets/post-menu.js index 36e9ea63f53..31ca63a89ac 100644 --- a/app/assets/javascripts/discourse/app/widgets/post-menu.js +++ b/app/assets/javascripts/discourse/app/widgets/post-menu.js @@ -354,6 +354,7 @@ registerButton("copyLink", () => { icon: "d-post-share", className: "post-action-menu__copy-link", title: "post.controls.copy_title", + ariaLive: "polite", }; }); diff --git a/app/assets/javascripts/discourse/ember-cli-build.js b/app/assets/javascripts/discourse/ember-cli-build.js index e9d6f0f40c2..b463a4876bc 100644 --- a/app/assets/javascripts/discourse/ember-cli-build.js +++ b/app/assets/javascripts/discourse/ember-cli-build.js @@ -13,6 +13,7 @@ const DeprecationSilencer = require("deprecation-silencer"); const { compatBuild } = require("@embroider/compat"); const { Webpack } = require("@embroider/webpack"); const { StatsWriterPlugin } = require("webpack-stats-plugin"); +const { RetryChunkLoadPlugin } = require("webpack-retry-chunk-load-plugin"); const withSideWatch = require("./lib/with-side-watch"); const RawHandlebarsCompiler = require("discourse-hbr/raw-handlebars-compiler"); const crypto = require("crypto"); @@ -223,6 +224,11 @@ module.exports = function (defaults) { return JSON.stringify(output, null, 2); }, }), + new RetryChunkLoadPlugin({ + retryDelay: 200, + maxRetries: 2, + chunks: ["assets/discourse.js"], + }), ], }, }, diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json index 295b1aa7b56..a57fd51496e 100644 --- a/app/assets/javascripts/discourse/package.json +++ b/app/assets/javascripts/discourse/package.json @@ -15,11 +15,11 @@ "test": "ember test" }, "dependencies": { - "@faker-js/faker": "^9.0.0", + "@faker-js/faker": "^9.0.3", "@glimmer/syntax": "^0.92.3", "@highlightjs/cdn-assets": "^11.10.0", "@json-editor/json-editor": "2.15.1", - "@messageformat/core": "^3.3.0", + "@messageformat/core": "^3.4.0", "@messageformat/runtime": "^3.0.1", "ace-builds": "^1.36.2", "decorator-transforms": "^2.0.0", @@ -35,8 +35,8 @@ "pretty-text": "workspace:1.0.0" }, "devDependencies": { - "@babel/core": "^7.25.2", - "@babel/standalone": "^7.25.6", + "@babel/core": "^7.25.7", + "@babel/standalone": "^7.25.7", "@colors/colors": "^1.6.0", "@discourse/backburner.js": "^2.7.1-0", "@discourse/itsatrap": "^2.0.10", @@ -47,17 +47,17 @@ "@ember/string": "^4.0.0", "@ember/test-helpers": "^4.0.4", "@ember/test-waiters": "^3.1.0", - "@embroider/compat": "^3.6.1", - "@embroider/core": "^3.4.15", - "@embroider/macros": "^1.13.1", + "@embroider/compat": "^3.6.2", + "@embroider/core": "^3.4.17", + "@embroider/macros": "^1.16.7", "@embroider/router": "^2.1.8", - "@embroider/webpack": "^4.0.5", - "@floating-ui/dom": "^1.6.10", + "@embroider/webpack": "^4.0.6", + "@floating-ui/dom": "^1.6.11", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", "@popperjs/core": "^2.11.8", "@swc/core": "^1.7.26", - "@types/jquery": "^3.5.30", + "@types/jquery": "^3.5.31", "@types/qunit": "^2.19.10", "@types/rsvp": "^4.0.9", "@uppy/aws-s3": "3.0.6", @@ -81,10 +81,10 @@ "discourse-i18n": "workspace:1.0.0", "discourse-markdown-it": "workspace:1.0.0", "discourse-plugins": "workspace:1.0.0", - "ember-auto-import": "^2.7.4", + "ember-auto-import": "^2.8.1", "ember-buffered-proxy": "^2.1.1", "ember-cached-decorator-polyfill": "^1.0.2", - "ember-cli": "~5.11.0", + "ember-cli": "~5.12.0", "ember-cli-app-version": "^7.0.0", "ember-cli-babel": "^8.2.0", "ember-cli-deprecation-workflow": "^3.0.2", @@ -95,36 +95,37 @@ "ember-cli-terser": "^4.0.2", "ember-decorators": "^6.1.1", "ember-exam": "^9.0.0", - "ember-load-initializers": "^2.1.1", + "ember-load-initializers": "^3.0.1", "ember-modifier": "^4.2.0", "ember-on-resize-modifier": "^2.0.2", "ember-production-deprecations": "workspace:1.0.0", "ember-qunit": "^8.1.0", "ember-source": "~5.5.0", - "ember-template-imports": "^4.1.1", + "ember-template-imports": "^4.1.2", "ember-test-selectors": "^7.0.0", "float-kit": "workspace:1.0.0", "html-entities": "^2.5.2", "imports-loader": "^5.0.0", "jquery": "^3.7.1", "js-yaml": "^4.1.0", - "jsuites": "^5.6.0", + "jsuites": "^5.6.5", "loader.js": "^4.7.0", "make-plural": "^7.4.0", "message-bus-client": "^4.3.8", "pretender": "^3.4.7", "qunit": "^2.22.0", - "qunit-dom": "^3.2.0", + "qunit-dom": "^3.2.1", "sass": "^1.77.7", "select-kit": "workspace:1.0.0", - "sinon": "^19.0.1", + "sinon": "^19.0.2", "source-map": "^0.7.4", - "terser": "^5.32.0", + "terser": "^5.34.1", "testem": "^3.15.2", "truth-helpers": "workspace:1.0.0", "util": "^0.12.5", "virtual-dom": "^2.1.1", - "webpack": "^5.94.0", + "webpack": "^5.95.0", + "webpack-retry-chunk-load-plugin": "^3.1.1", "webpack-stats-plugin": "^1.1.3", "xss": "^1.0.15" }, diff --git a/app/assets/javascripts/discourse/tests/acceptance/dashboard-test.js b/app/assets/javascripts/discourse/tests/acceptance/dashboard-test.js index 270acddc74c..e494aa7f85b 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/dashboard-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/dashboard-test.js @@ -4,7 +4,6 @@ import { acceptance, count, exists, - query, } from "discourse/tests/helpers/qunit-helpers"; import selectKit from "discourse/tests/helpers/select-kit-helper"; @@ -60,13 +59,6 @@ acceptance("Dashboard", function (needs) { exists(".admin-report.new-contributors"), "new-contributors report" ); - assert.strictEqual( - query( - ".section.dashboard-problems .problem-messages ul li:first-child" - ).innerHTML.trim(), - "Houston...", - "displays problems" - ); }); test("moderation tab", async function (assert) { diff --git a/app/assets/javascripts/discourse/tests/helpers/form-kit-assertions.js b/app/assets/javascripts/discourse/tests/helpers/form-kit-assertions.js index acdd8bc02b1..7d6c3733414 100644 --- a/app/assets/javascripts/discourse/tests/helpers/form-kit-assertions.js +++ b/app/assets/javascripts/discourse/tests/helpers/form-kit-assertions.js @@ -142,12 +142,6 @@ class FieldHelper { } } - hasSubtitle(subtitle, message) { - this.context - .dom(this.element.querySelector(".form-kit__container-subtitle")) - .hasText(subtitle, message); - } - hasDescription(description, message) { switch (this.element.dataset.controlType) { case "checkbox": { @@ -162,7 +156,7 @@ class FieldHelper { } default: { this.context - .dom(this.element.querySelector(".form-kit__meta-description")) + .dom(this.element.querySelector(".form-kit__container-description")) .hasText(description, message); } } diff --git a/app/assets/javascripts/discourse/tests/integration/components/form-kit/controls/custom-test.gjs b/app/assets/javascripts/discourse/tests/integration/components/form-kit/controls/custom-test.gjs index 615e38d0139..6407f4b2ef6 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/form-kit/controls/custom-test.gjs +++ b/app/assets/javascripts/discourse/tests/integration/components/form-kit/controls/custom-test.gjs @@ -11,7 +11,7 @@ module( test("default", async function (assert) { await render(); assert.dom(".form-kit__container-title").hasText("Foo (optional)"); - assert.dom(".form-kit__container-subtitle").hasText("Bar"); + assert.dom(".form-kit__container-description").hasText("Bar"); }); } ); diff --git a/app/assets/javascripts/discourse/tests/integration/components/form-kit/field-test.gjs b/app/assets/javascripts/discourse/tests/integration/components/form-kit/field-test.gjs index 414a3ab7dd1..31e05d091e2 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/form-kit/field-test.gjs +++ b/app/assets/javascripts/discourse/tests/integration/components/form-kit/field-test.gjs @@ -35,18 +35,6 @@ module("Integration | Component | FormKit | Field", function (hooks) { assert.dom(".form-kit__row .form-kit__col.--col-8").hasText("Test"); }); - test("@subtitle", async function (assert) { - await render(); - - assert.form().field("foo").hasSubtitle("foo foo"); - }); - test("@description", async function (assert) { await render(