diff --git a/app/assets/javascripts/discourse/app/components/modal/avatar-selector.hbs b/app/assets/javascripts/discourse/app/components/modal/avatar-selector.hbs
new file mode 100644
index 00000000000..45599c55bec
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/modal/avatar-selector.hbs
@@ -0,0 +1,141 @@
+
+ <:body>
+ {{#if this.showSelectableAvatars}}
+
+ {{#if this.showAvatarUploader}}
+ {{i18n "user.change_avatar.use_custom"}}
+ {{/if}}
+ {{/if}}
+ {{#if this.showAvatarUploader}}
+ {{#if this.user.use_logo_small_as_avatar}}
+
+
+
+
+ {{/if}}
+
+
+
+
+ {{#if this.allowAvatarUpload}}
+
+
+
+
+
+
+ {{#if this.gravatarFailed}}
+
+ {{i18n
+ "user.change_avatar.gravatar_failed"
+ gravatarName=this.siteSettings.gravatar_name
+ }}
+
+ {{/if}}
+
+
+
+
+
+
+ {{/if}}
+ {{/if}}
+
+
+ <:footer>
+ {{#if this.showAvatarUploader}}
+
+
+ {{/if}}
+
+
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/app/components/modal/avatar-selector.js b/app/assets/javascripts/discourse/app/components/modal/avatar-selector.js
new file mode 100644
index 00000000000..8a7043f1e01
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/modal/avatar-selector.js
@@ -0,0 +1,175 @@
+import Component from "@glimmer/component";
+import { tracked } from "@glimmer/tracking";
+import { action } from "@ember/object";
+import { inject as service } from "@ember/service";
+import { ajax } from "discourse/lib/ajax";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import { allowsImages } from "discourse/lib/uploads";
+import { isTesting } from "discourse-common/config/environment";
+
+export default class AvatarSelectorModal extends Component {
+ @service currentUser;
+ @service siteSettings;
+ @tracked gravatarRefreshDisabled = false;
+ @tracked gravatarFailed = false;
+ @tracked uploading = false;
+ @tracked _selected = null;
+
+ get user() {
+ return this.args.model.user;
+ }
+
+ get selected() {
+ return this._selected ?? this.defaultSelection;
+ }
+
+ set selected(value) {
+ this._selected = value;
+ }
+
+ get submitDisabled() {
+ return this.selected === "logo" || this.uploading;
+ }
+
+ get selectableAvatars() {
+ const mode = this.siteSettings.selectable_avatars_mode;
+ const list = this.siteSettings.selectable_avatars;
+ return mode !== "disabled" ? (list ? list.split("|") : []) : null;
+ }
+
+ get showSelectableAvatars() {
+ return this.siteSettings.selectable_avatars_mode !== "disabled";
+ }
+
+ get showAvatarUploader() {
+ const mode = this.siteSettings.selectable_avatars_mode;
+ switch (mode) {
+ case "no_one":
+ return false;
+ case "tl1":
+ case "tl2":
+ case "tl3":
+ case "tl4":
+ const allowedTl = parseInt(mode.replace("tl", ""), 10);
+ return (
+ this.user.admin ||
+ this.user.moderator ||
+ this.user.trust_level >= allowedTl
+ );
+ case "staff":
+ return this.user.admin || this.user.moderator;
+ case "everyone":
+ default:
+ return true;
+ }
+ }
+
+ get defaultSelection() {
+ if (this.user.use_logo_small_as_avatar) {
+ return "logo";
+ } else if (this.user.avatar_template === this.user.system_avatar_template) {
+ return "system";
+ } else if (
+ this.user.avatar_template === this.user.gravatar_avatar_template
+ ) {
+ return "gravatar";
+ } else {
+ return "custom";
+ }
+ }
+
+ get selectedUploadId() {
+ const selected = this.selected;
+ switch (selected) {
+ case "system":
+ return this.user.system_avatar_upload_id;
+ case "gravatar":
+ return this.user.gravatar_avatar_upload_id;
+ default:
+ return this.user.custom_avatar_upload_id;
+ }
+ }
+
+ get allowAvatarUpload() {
+ return (
+ this.siteSettingMatches &&
+ allowsImages(this.currentUser.staff, this.siteSettings)
+ );
+ }
+
+ get siteSettingMatches() {
+ const allowUploadedAvatars = this.siteSettings.allow_uploaded_avatars;
+ switch (allowUploadedAvatars) {
+ case "disabled":
+ return false;
+ case "staff":
+ return this.currentUser.staff;
+ case "admin":
+ return this.currentUser.admin;
+ default:
+ return (
+ this.currentUser.trust_level >= parseInt(allowUploadedAvatars, 10) ||
+ this.currentUser.staff
+ );
+ }
+ }
+
+ @action
+ onSelectedChanged(value) {
+ this.selected = value;
+ }
+
+ @action
+ async selectAvatar(url, event) {
+ event?.preventDefault();
+ try {
+ await this.user.selectAvatar(url);
+ window.location.reload();
+ } catch (error) {
+ popupAjaxError(error);
+ }
+ }
+
+ @action
+ uploadComplete() {
+ this.selected = "custom";
+ }
+
+ @action
+ async refreshGravatar() {
+ this.gravatarRefreshDisabled = true;
+
+ try {
+ const result = await ajax(
+ `/user_avatar/${this.user.username}/refresh_gravatar.json`,
+ {
+ type: "POST",
+ }
+ );
+
+ if (!result.gravatar_upload_id) {
+ this.gravatarFailed = true;
+ } else {
+ this.gravatarFailed = false;
+ this.user.setProperties({
+ gravatar_avatar_upload_id: result.gravatar_upload_id,
+ gravatar_avatar_template: result.gravatar_avatar_template,
+ });
+ }
+ } finally {
+ this.gravatarRefreshDisabled = false;
+ }
+ }
+
+ @action
+ async saveAvatarSelection() {
+ try {
+ await this.user.pickAvatar(this.selectedUploadId, this.selected);
+ if (!isTesting()) {
+ window.location.reload();
+ }
+ } catch (error) {
+ popupAjaxError(error);
+ }
+ }
+}
diff --git a/app/assets/javascripts/discourse/app/controllers/avatar-selector.js b/app/assets/javascripts/discourse/app/controllers/avatar-selector.js
deleted file mode 100644
index d88cbfe62bd..00000000000
--- a/app/assets/javascripts/discourse/app/controllers/avatar-selector.js
+++ /dev/null
@@ -1,200 +0,0 @@
-import { tracked } from "@glimmer/tracking";
-import Controller from "@ember/controller";
-import { action } from "@ember/object";
-import { dependentKeyCompat } from "@ember/object/compat";
-import { ajax } from "discourse/lib/ajax";
-import { popupAjaxError } from "discourse/lib/ajax-error";
-import { setting } from "discourse/lib/computed";
-import { allowsImages } from "discourse/lib/uploads";
-import ModalFunctionality from "discourse/mixins/modal-functionality";
-import { isTesting } from "discourse-common/config/environment";
-import discourseComputed from "discourse-common/utils/decorators";
-
-export default Controller.extend(ModalFunctionality, {
- gravatarName: setting("gravatar_name"),
- gravatarBaseUrl: setting("gravatar_base_url"),
- gravatarLoginUrl: setting("gravatar_login_url"),
-
- @discourseComputed("selected", "uploading")
- submitDisabled(selected, uploading) {
- return selected === "logo" || uploading;
- },
-
- @discourseComputed(
- "siteSettings.selectable_avatars_mode",
- "siteSettings.selectable_avatars"
- )
- selectableAvatars(mode, list) {
- if (mode !== "disabled") {
- return list ? list.split("|") : [];
- }
- },
-
- @discourseComputed("siteSettings.selectable_avatars_mode")
- showSelectableAvatars(mode) {
- return mode !== "disabled";
- },
-
- @discourseComputed("siteSettings.selectable_avatars_mode")
- showAvatarUploader(mode) {
- switch (mode) {
- case "no_one":
- return false;
- case "tl1":
- case "tl2":
- case "tl3":
- case "tl4":
- const allowedTl = parseInt(mode.replace("tl", ""), 10);
- return (
- this.user.admin ||
- this.user.moderator ||
- this.user.trust_level >= allowedTl
- );
- case "staff":
- return this.user.admin || this.user.moderator;
- case "everyone":
- default:
- return true;
- }
- },
-
- @tracked _selected: null,
-
- @dependentKeyCompat
- get selected() {
- return this._selected ?? this.defaultSelection;
- },
-
- set selected(value) {
- this._selected = value;
- },
-
- @action
- onSelectedChanged(value) {
- this._selected = value;
- },
-
- get defaultSelection() {
- if (this.get("user.use_logo_small_as_avatar")) {
- return "logo";
- } else if (
- this.get("user.avatar_template") ===
- this.get("user.system_avatar_template")
- ) {
- return "system";
- } else if (
- this.get("user.avatar_template") ===
- this.get("user.gravatar_avatar_template")
- ) {
- return "gravatar";
- } else {
- return "custom";
- }
- },
-
- @discourseComputed(
- "selected",
- "user.system_avatar_upload_id",
- "user.gravatar_avatar_upload_id",
- "user.custom_avatar_upload_id"
- )
- selectedUploadId(selected, system, gravatar, custom) {
- switch (selected) {
- case "system":
- return system;
- case "gravatar":
- return gravatar;
- default:
- return custom;
- }
- },
-
- @discourseComputed(
- "selected",
- "user.system_avatar_template",
- "user.gravatar_avatar_template",
- "user.custom_avatar_template"
- )
- selectedAvatarTemplate(selected, system, gravatar, custom) {
- switch (selected) {
- case "system":
- return system;
- case "gravatar":
- return gravatar;
- default:
- return custom;
- }
- },
-
- siteSettingMatches(value, user) {
- switch (value) {
- case "disabled":
- return false;
- case "staff":
- return user.staff;
- case "admin":
- return user.admin;
- default:
- return user.trust_level >= parseInt(value, 10) || user.staff;
- }
- },
-
- @discourseComputed("siteSettings.allow_uploaded_avatars")
- allowAvatarUpload(allowUploadedAvatars) {
- return (
- this.siteSettingMatches(allowUploadedAvatars, this.currentUser) &&
- allowsImages(this.currentUser.staff, this.siteSettings)
- );
- },
-
- @action
- selectAvatar(url, event) {
- event?.preventDefault();
- this.user
- .selectAvatar(url)
- .then(() => window.location.reload())
- .catch(popupAjaxError);
- },
-
- actions: {
- uploadComplete() {
- this.set("selected", "custom");
- },
-
- refreshGravatar() {
- this.set("gravatarRefreshDisabled", true);
-
- return ajax(
- `/user_avatar/${this.get("user.username")}/refresh_gravatar.json`,
- { type: "POST" }
- )
- .then((result) => {
- if (!result.gravatar_upload_id) {
- this.set("gravatarFailed", true);
- } else {
- this.set("gravatarFailed", false);
-
- this.user.setProperties({
- gravatar_avatar_upload_id: result.gravatar_upload_id,
- gravatar_avatar_template: result.gravatar_avatar_template,
- });
- }
- })
- .finally(() => this.set("gravatarRefreshDisabled", false));
- },
-
- saveAvatarSelection() {
- const selectedUploadId = this.selectedUploadId;
- const type = this.selected;
-
- this.user
- .pickAvatar(selectedUploadId, type)
- .then(() => {
- if (!isTesting()) {
- window.location.reload();
- }
- })
- .catch(popupAjaxError);
- },
- },
-});
diff --git a/app/assets/javascripts/discourse/app/routes/preferences-account.js b/app/assets/javascripts/discourse/app/routes/preferences-account.js
index 8140763a447..04d6dab5fe5 100644
--- a/app/assets/javascripts/discourse/app/routes/preferences-account.js
+++ b/app/assets/javascripts/discourse/app/routes/preferences-account.js
@@ -1,10 +1,12 @@
import { action } from "@ember/object";
-import showModal from "discourse/lib/show-modal";
+import { inject as service } from "@ember/service";
+import AvatarSelectorModal from "discourse/components/modal/avatar-selector";
import UserBadge from "discourse/models/user-badge";
import RestrictedUserRoute from "discourse/routes/restricted-user";
import I18n from "discourse-i18n";
export default RestrictedUserRoute.extend({
+ modal: service(),
model() {
const user = this.modelFor("user");
if (this.siteSettings.enable_badges) {
@@ -37,6 +39,8 @@ export default RestrictedUserRoute.extend({
@action
showAvatarSelector(user) {
- showModal("avatar-selector").setProperties({ user });
+ this.modal.show(AvatarSelectorModal, {
+ model: { user },
+ });
},
});
diff --git a/app/assets/javascripts/discourse/app/templates/modal/avatar-selector.hbs b/app/assets/javascripts/discourse/app/templates/modal/avatar-selector.hbs
deleted file mode 100644
index 81d7d33d533..00000000000
--- a/app/assets/javascripts/discourse/app/templates/modal/avatar-selector.hbs
+++ /dev/null
@@ -1,130 +0,0 @@
-
- {{#if this.showSelectableAvatars}}
-
- {{#if this.showAvatarUploader}}
- {{html-safe (i18n "user.change_avatar.use_custom")}}
- {{/if}}
- {{/if}}
- {{#if this.showAvatarUploader}}
- {{#if this.user.use_logo_small_as_avatar}}
-
-
-
-
- {{/if}}
-
-
-
-
- {{#if this.allowAvatarUpload}}
-
-
-
-
-
-
- {{#if this.gravatarFailed}}
-
{{I18n
- "user.change_avatar.gravatar_failed"
- gravatarName=this.gravatarName
- }}
- {{/if}}
-
-
-
-
-
-
- {{/if}}
- {{/if}}
-
-
-{{#if this.showAvatarUploader}}
-
-{{/if}}
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/tests/unit/controllers/avatar-selector-test.js b/app/assets/javascripts/discourse/tests/unit/controllers/avatar-selector-test.js
deleted file mode 100644
index da8041212e5..00000000000
--- a/app/assets/javascripts/discourse/tests/unit/controllers/avatar-selector-test.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import EmberObject from "@ember/object";
-import { setupTest } from "ember-qunit";
-import { module, test } from "qunit";
-
-module("Unit | Controller | avatar-selector", function (hooks) {
- setupTest(hooks);
-
- test("avatarTemplate", function (assert) {
- const user = EmberObject.create({
- avatar_template: "avatar",
- system_avatar_template: "system",
- gravatar_avatar_template: "gravatar",
-
- system_avatar_upload_id: 1,
- gravatar_avatar_upload_id: 2,
- custom_avatar_upload_id: 3,
- });
- const controller = this.owner.lookup("controller:avatar-selector");
- controller.setProperties({ user });
-
- user.set("avatar_template", "system");
- assert.strictEqual(
- controller.selectedUploadId,
- 1,
- "we are using system by default"
- );
-
- user.set("avatar_template", "gravatar");
- assert.strictEqual(
- controller.selectedUploadId,
- 2,
- "we are using gravatar when set"
- );
-
- user.set("avatar_template", "avatar");
- assert.strictEqual(
- controller.selectedUploadId,
- 3,
- "we are using custom when set"
- );
- });
-});
diff --git a/spec/system/page_objects/modals/avatar_selector.rb b/spec/system/page_objects/modals/avatar_selector.rb
new file mode 100644
index 00000000000..b422bcf2b8d
--- /dev/null
+++ b/spec/system/page_objects/modals/avatar_selector.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+module PageObjects
+ module Modals
+ class AvatarSelector < PageObjects::Modals::Base
+ BODY_SELECTOR = ".avatar-selector"
+ MODAL_SELECTOR = ".avatar-selector-modal"
+
+ def select_avatar_upload_option
+ body.choose("avatar", option: "custom")
+ end
+
+ def select_system_assigned_option
+ body.choose("avatar", option: "system")
+ end
+
+ def click_avatar_upload_button
+ body.find_button(I18n.t("js.user.change_avatar.upload_title")).click
+ end
+
+ def has_user_avatar_image_uploaded?
+ body.has_css?(".avatar[src*='uploads/default']")
+ end
+ end
+ end
+end
diff --git a/spec/system/page_objects/pages/user_preferences_account.rb b/spec/system/page_objects/pages/user_preferences_account.rb
new file mode 100644
index 00000000000..bc142022016
--- /dev/null
+++ b/spec/system/page_objects/pages/user_preferences_account.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module PageObjects
+ module Pages
+ class UserPreferencesAccount < PageObjects::Pages::Base
+ def visit(user)
+ page.visit("/u/#{user.username}/preferences/account")
+ self
+ end
+
+ def click_edit_avatar_button
+ page.find_button("edit-avatar").click
+ end
+
+ def open_avatar_selector_modal(user)
+ visit(user).click_edit_avatar_button
+ end
+
+ def has_custom_uploaded_avatar_image?
+ has_css?(".pref-avatar img.avatar[src*='user_avatar']")
+ end
+
+ def has_system_avatar_image?
+ has_css?(".pref-avatar img.avatar[src*='letter_avatar']")
+ end
+ end
+ end
+end
diff --git a/spec/system/user_page/user_preferences_account_spec.rb b/spec/system/user_page/user_preferences_account_spec.rb
new file mode 100644
index 00000000000..79b32201d45
--- /dev/null
+++ b/spec/system/user_page/user_preferences_account_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+describe "User preferences for Account", type: :system do
+ fab!(:user) { Fabricate(:user) }
+ let(:user_account_preferences_page) { PageObjects::Pages::UserPreferencesAccount.new }
+ let(:avatar_selector_modal) { PageObjects::Modals::AvatarSelector.new }
+ before { sign_in(user) }
+
+ describe "avatar-selector modal" do
+ it "saves custom picture and system assigned pictures" do
+ user_account_preferences_page.open_avatar_selector_modal(user)
+ expect(avatar_selector_modal).to be_open
+
+ avatar_selector_modal.select_avatar_upload_option
+ file_path = File.absolute_path(file_from_fixtures("logo.jpg"))
+ attach_file(file_path) { avatar_selector_modal.click_avatar_upload_button }
+ expect(avatar_selector_modal).to have_user_avatar_image_uploaded
+ avatar_selector_modal.click_primary_button
+ expect(avatar_selector_modal).to be_closed
+ expect(user_account_preferences_page).to have_custom_uploaded_avatar_image
+
+ user_account_preferences_page.open_avatar_selector_modal(user)
+ avatar_selector_modal.select_system_assigned_option
+ avatar_selector_modal.click_primary_button
+ expect(avatar_selector_modal).to be_closed
+ expect(user_account_preferences_page).to have_system_avatar_image
+ end
+ end
+end