From 3c7e7fc657940119d9cde1c3a111af5eafe90857 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Wed, 7 Jan 2026 17:34:34 +0100 Subject: [PATCH 1/2] Simplify user avatar upload ref https://www.woltlab.com/community/thread/315385-korrektes-event-bei-wcf-command-user-saveavatar/ --- .../lib/action/UserAvatarAction.class.php | 64 +------------------ wcfsetup/install/lang/de.xml | 4 +- wcfsetup/install/lang/en.xml | 4 +- 3 files changed, 5 insertions(+), 67 deletions(-) diff --git a/wcfsetup/install/files/lib/action/UserAvatarAction.class.php b/wcfsetup/install/files/lib/action/UserAvatarAction.class.php index 62316ee033..1fc925a6eb 100644 --- a/wcfsetup/install/files/lib/action/UserAvatarAction.class.php +++ b/wcfsetup/install/files/lib/action/UserAvatarAction.class.php @@ -2,23 +2,16 @@ namespace wcf\action; -use Laminas\Diactoros\Response\JsonResponse; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; -use wcf\data\IStorableObject; use wcf\data\user\UserProfile; use wcf\http\Helper; use wcf\system\cache\runtime\UserProfileRuntimeCache; use wcf\system\exception\NamedUserException; use wcf\system\exception\PermissionDeniedException; -use wcf\system\form\builder\data\processor\CustomFormDataProcessor; -use wcf\system\form\builder\field\dependency\ValueFormFieldDependency; use wcf\system\form\builder\field\FileProcessorFormField; -use wcf\system\form\builder\field\RadioButtonFormField; -use wcf\system\form\builder\IFormDocument; use wcf\system\form\builder\Psr15DialogForm; -use wcf\command\user\SetAvatar; use wcf\system\user\UserProfileHandler; use wcf\system\WCF; use wcf\util\HtmlString; @@ -69,31 +62,6 @@ public function handle(ServerRequestInterface $request): ResponseInterface if ($request->getMethod() === 'GET') { return $form->toResponse(); - } elseif ($request->getMethod() === 'POST') { - $response = $form->validateRequest($request); - if ($response !== null) { - return $response; - } - - $data = $form->getData()['data']; - - // If the user has already uploaded and optionally cropped an image, - // this is already assigned to the `$user` and does not need to be saved again. - // However, if the user wants to delete their avatar and use a standard avatar, - // this must be saved and the cache reset - if ($data['avatarType'] === 'none') { - (new SetAvatar($user->getDecoratedObject()))(); - } - - // Reload the user object to get the updated avatar - UserProfileRuntimeCache::getInstance()->removeObject($user->userID); - $user = UserProfileRuntimeCache::getInstance()->getObject($user->userID); - - return new JsonResponse([ - 'result' => [ - 'avatar' => $user->getAvatar()->getURL(), - ], - ]); } else { throw new \LogicException('Unreachable'); } @@ -106,44 +74,14 @@ private function getForm(UserProfile $user): Psr15DialogForm WCF::getLanguage()->get('wcf.user.avatar.edit') ); $form->appendChildren([ - RadioButtonFormField::create('avatarType') - ->value("none") - ->required() - ->options([ - "none" => WCF::getLanguage()->get('wcf.user.avatar.type.none'), - "custom" => WCF::getLanguage()->get('wcf.user.avatar.type.custom'), - ]), FileProcessorFormField::create('avatarFileID') ->objectType('com.woltlab.wcf.user.avatar') ->description('wcf.user.avatar.type.custom.description') - ->required() ->singleFileUpload() ->bigPreview() ->simpleReplace() - ->hideDeleteButton() - ->thumbnailSize('128') - ->addDependency( - ValueFormFieldDependency::create('avatarType') - ->fieldId('avatarType') - ->values(['custom']) - ), + ->thumbnailSize('128'), ]); - $form->getDataHandler()->addProcessor( - new CustomFormDataProcessor( - 'avatarType', - null, - function (IFormDocument $document, array $data, IStorableObject $object) { - \assert($object instanceof UserProfile); - if ($object->avatarFileID === null) { - $data['avatarType'] = 'none'; - } else { - $data['avatarType'] = 'custom'; - } - - return $data; - } - ) - ); $form->markRequiredFields(false); $form->updatedObject($user); diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index f1410d0b8c..3cb8e011b3 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -5257,9 +5257,7 @@ Sobald {if LANGUAGE_USE_INFORMAL_VARIANT}dein{else}Ihr{/if} Benutzerkonto freige user->avatarID} {if LANGUAGE_USE_INFORMAL_VARIANT}deinen{else}Ihren{/if} derzeitigen Avatar gesperrt und{/if} {if LANGUAGE_USE_INFORMAL_VARIANT}dir{else}Ihnen{/if} die weitere Nutzungsberechtigung der Avatar-Funktion {if !$__wcf->user->disableAvatarReason}entzogen.{else} aus folgenden Gründen entzogen: {$__wcf->user->disableAvatarReason}{/if}]]> - session->getPermission('user.profile.avatar.allowedFileExtensions')} besitzen.
Die Mindestgröße für Avatare liegt bei 128 × 128 Pixel.]]>
- @@ -5727,5 +5725,7 @@ Erlaubte Dateiendungen: {', '|implode:$allowedFileExtensions}]]> + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 1d092dcd2a..b40a651f4a 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -5256,9 +5256,7 @@ You also received a list of backup codes to use when your second factor becomes user->avatarID}have blocked your avatar and {/if}disallowed you from using an avatar{if $__wcf->user->disableAvatarReason}: {$__wcf->user->disableAvatarReason}{/if}.]]> - session->getPermission('user.profile.avatar.allowedFileExtensions')}” for your avatar.
The minimum dimensions are 128 × 128 pixels.]]>
-
@@ -5729,5 +5727,7 @@ Allowed extensions: {', '|implode:$allowedFileExtensions}]]> + + From 480d012a32aa332b824fbff4c720d9428e66b739 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Wed, 7 Jan 2026 17:56:26 +0100 Subject: [PATCH 2/2] Align the avatar management with the cover photo --- ts/WoltLabSuite/Core/Component/User/Avatar.ts | 65 ++++++++++--------- .../install/files/acp/templates/userAdd.tpl | 2 +- .../Core/Component/User/Avatar.js | 49 +++++++------- 3 files changed, 59 insertions(+), 57 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/User/Avatar.ts b/ts/WoltLabSuite/Core/Component/User/Avatar.ts index dac612ac44..f63a7734e1 100644 --- a/ts/WoltLabSuite/Core/Component/User/Avatar.ts +++ b/ts/WoltLabSuite/Core/Component/User/Avatar.ts @@ -14,29 +14,30 @@ import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog"; import { registerCallback } from "WoltLabSuite/Core/Form/Builder/Field/Controller/FileProcessor"; import WoltlabCoreFile from "WoltLabSuite/Core/Component/File/woltlab-core-file"; import { showDefaultSuccessSnackbar } from "../Snackbar"; +import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import * as FormBuilderManager from "WoltLabSuite/Core/Form/Builder/Manager"; -interface Result { - avatar: string; -} +type ResponseGetForm = { + dialog: string; + formId: string; + title: string; +}; + +let defaultAvatar = ""; async function editAvatar(button: HTMLElement): Promise { - const { ok, result } = await dialogFactory().usingFormBuilder().fromEndpoint(button.dataset.editAvatar!); + defaultAvatar = button.dataset.defaultAvatar || ""; - if (ok) { - const avatarForm = document.getElementById("avatarForm"); - if (avatarForm) { - const img = avatarForm.querySelector("img.userAvatarImage")!; - if (img.src === result.avatar) { - return; - } + const json = (await prepareRequest(button.dataset.editAvatar!).get().fetchAsJson()) as ResponseGetForm; + const dialog = dialogFactory().fromHtml(json.dialog).withoutControls(); - // In the ACP, the form should not be reloaded after changing the avatar. - img.src = result.avatar; - showDefaultSuccessSnackbar(); - } else { - window.location.reload(); + dialog.addEventListener("afterClose", () => { + if (FormBuilderManager.hasForm(json.formId)) { + FormBuilderManager.unregisterForm(json.formId); } - } + }); + + dialog.show(json.title); } export function setup(): void { @@ -51,28 +52,32 @@ export function setup(): void { const avatarForm = document.getElementById("avatarForm"); if (avatarForm) { registerCallback("wcf\\action\\UserAvatarAction_avatarFileID", (fileId: number | undefined) => { - if (!fileId) { - return; + let link = defaultAvatar; + if (fileId !== undefined) { + const file = document.querySelector( + `#wcf\\\\action\\\\UserAvatarAction_avatarFileIDContainer woltlab-core-file[file-id="${fileId}"]`, + )!; + + link = file.link!; } - const file = document.querySelector( - `#wcf\\\\action\\\\UserAvatarAction_avatarFileIDContainer woltlab-core-file[file-id="${fileId}"]`, - )!; + avatarForm.querySelector("img.userAvatarImage")!.src = link; + + document + .getElementById("wcf\\action\\UserAvatarAction_avatarFileIDContainer") + ?.closest("woltlab-core-dialog") + ?.close(); - avatarForm.querySelector("img.userAvatarImage")!.src = file.link!; showDefaultSuccessSnackbar(); }); } else { - registerCallback("wcf\\action\\UserAvatarAction_avatarFileID", (fileId: number | undefined) => { - if (fileId === undefined) { - return; - } - + registerCallback("wcf\\action\\UserAvatarAction_avatarFileID", () => { document .getElementById("wcf\\action\\UserAvatarAction_avatarFileIDContainer") ?.closest("woltlab-core-dialog") - ?.querySelector(".dialog__control__button--primary") - ?.click(); + ?.close(); + + window.location.reload(); }); } diff --git a/wcfsetup/install/files/acp/templates/userAdd.tpl b/wcfsetup/install/files/acp/templates/userAdd.tpl index 2a82a78f0b..83a70d26bd 100644 --- a/wcfsetup/install/files/acp/templates/userAdd.tpl +++ b/wcfsetup/install/files/acp/templates/userAdd.tpl @@ -614,7 +614,7 @@
-
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/User/Avatar.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/User/Avatar.js index 5979f23c6c..7c044ec97e 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/User/Avatar.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/User/Avatar.js @@ -7,27 +7,22 @@ * @since 6.2 * @woltlabExcludeBundle all */ -define(["require", "exports", "WoltLabSuite/Core/Helper/PromiseMutex", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Component/Dialog", "WoltLabSuite/Core/Form/Builder/Field/Controller/FileProcessor", "../Snackbar"], function (require, exports, PromiseMutex_1, Selector_1, Dialog_1, FileProcessor_1, Snackbar_1) { +define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/PromiseMutex", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Component/Dialog", "WoltLabSuite/Core/Form/Builder/Field/Controller/FileProcessor", "../Snackbar", "WoltLabSuite/Core/Ajax/Backend", "WoltLabSuite/Core/Form/Builder/Manager"], function (require, exports, tslib_1, PromiseMutex_1, Selector_1, Dialog_1, FileProcessor_1, Snackbar_1, Backend_1, FormBuilderManager) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setup = setup; + FormBuilderManager = tslib_1.__importStar(FormBuilderManager); + let defaultAvatar = ""; async function editAvatar(button) { - const { ok, result } = await (0, Dialog_1.dialogFactory)().usingFormBuilder().fromEndpoint(button.dataset.editAvatar); - if (ok) { - const avatarForm = document.getElementById("avatarForm"); - if (avatarForm) { - const img = avatarForm.querySelector("img.userAvatarImage"); - if (img.src === result.avatar) { - return; - } - // In the ACP, the form should not be reloaded after changing the avatar. - img.src = result.avatar; - (0, Snackbar_1.showDefaultSuccessSnackbar)(); - } - else { - window.location.reload(); + defaultAvatar = button.dataset.defaultAvatar || ""; + const json = (await (0, Backend_1.prepareRequest)(button.dataset.editAvatar).get().fetchAsJson()); + const dialog = (0, Dialog_1.dialogFactory)().fromHtml(json.dialog).withoutControls(); + dialog.addEventListener("afterClose", () => { + if (FormBuilderManager.hasForm(json.formId)) { + FormBuilderManager.unregisterForm(json.formId); } - } + }); + dialog.show(json.title); } function setup() { (0, Selector_1.wheneverFirstSeen)(`#wcf\\\\action\\\\UserAvatarAction_avatarFileIDContainer woltlab-core-file img`, (img) => { @@ -37,24 +32,26 @@ define(["require", "exports", "WoltLabSuite/Core/Helper/PromiseMutex", "WoltLabS const avatarForm = document.getElementById("avatarForm"); if (avatarForm) { (0, FileProcessor_1.registerCallback)("wcf\\action\\UserAvatarAction_avatarFileID", (fileId) => { - if (!fileId) { - return; + let link = defaultAvatar; + if (fileId !== undefined) { + const file = document.querySelector(`#wcf\\\\action\\\\UserAvatarAction_avatarFileIDContainer woltlab-core-file[file-id="${fileId}"]`); + link = file.link; } - const file = document.querySelector(`#wcf\\\\action\\\\UserAvatarAction_avatarFileIDContainer woltlab-core-file[file-id="${fileId}"]`); - avatarForm.querySelector("img.userAvatarImage").src = file.link; + avatarForm.querySelector("img.userAvatarImage").src = link; + document + .getElementById("wcf\\action\\UserAvatarAction_avatarFileIDContainer") + ?.closest("woltlab-core-dialog") + ?.close(); (0, Snackbar_1.showDefaultSuccessSnackbar)(); }); } else { - (0, FileProcessor_1.registerCallback)("wcf\\action\\UserAvatarAction_avatarFileID", (fileId) => { - if (fileId === undefined) { - return; - } + (0, FileProcessor_1.registerCallback)("wcf\\action\\UserAvatarAction_avatarFileID", () => { document .getElementById("wcf\\action\\UserAvatarAction_avatarFileIDContainer") ?.closest("woltlab-core-dialog") - ?.querySelector(".dialog__control__button--primary") - ?.click(); + ?.close(); + window.location.reload(); }); } (0, Selector_1.wheneverFirstSeen)("[data-edit-avatar]", (button) => {