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}} +
+ {{#each this.selectableAvatars as |avatar|}} + + {{bound-avatar-template avatar "huge"}} + + {{/each}} +
+ {{#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}} -
- {{#each this.selectableAvatars as |avatar|}} - - {{bound-avatar-template avatar "huge"}} - - {{/each}} -
- {{#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