Add dialog to edit the avatar file id
authorCyperghost <olaf_schmitz_1@t-online.de>
Thu, 7 Nov 2024 10:55:02 +0000 (11:55 +0100)
committerCyperghost <olaf_schmitz_1@t-online.de>
Thu, 7 Nov 2024 10:55:02 +0000 (11:55 +0100)
com.woltlab.wcf/templates/pageHeaderUser.tpl
ts/WoltLabSuite/Core/BootstrapFrontend.ts
ts/WoltLabSuite/Core/Component/User/Avatar.ts [new file with mode: 0644]
ts/WoltLabSuite/Core/Ui/User/Menu/ControlPanel.ts
wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js
wcfsetup/install/files/js/WoltLabSuite/Core/Component/User/Avatar.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Menu/ControlPanel.js
wcfsetup/install/files/lib/action/UserAvatarAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/file/processor/UserAvatarFileProcessor.class.php

index b2a4490bb021724275f62fe295cd5ffa0296c164..5f0f20e035a40c310d38e25459038e3d8dbe7c45 100644 (file)
                                                                        </div>
                                                                {/if}
                                                                {if $__wcf->getSession()->getPermission('user.profile.avatar.canUploadAvatar') && !$__wcf->getUserProfileHandler()->disableAvatar}
-                                                                       <div class="userMenuItem userMenuItemNarrow userMenuItemSingleLine">
+                                                                       <div class="userMenuItem userMenuItemNarrow userMenuItemSingleLine userAvatarManagement" data-edit-avatar="{link controller="UserAvatar"}{/link}">
                                                                                <div class="userMenuItemImage">
                                                                                        {icon size=16 name='user-pen'}
                                                                                </div>
                                                                                <div class="userMenuItemContent">
-                                                                                       <button type="button" class="userMenuItemLink userAvatarManagement">
+                                                                                       <button type="button" class="userMenuItemLink">
                                                                                                {lang}wcf.user.avatarManagement{/lang}
                                                                                        </button>
                                                                                </div>
index 01840f3dc6a288288bf7760b510985f7ac58844f..589d75b7ddaec7a4d55b42c0bb09cfc244672ec6 100644 (file)
@@ -136,4 +136,7 @@ export function setup(options: BootstrapOptions): void {
   whenFirstSeen("[data-ignore-user]", () => {
     void import("./Component/User/Ignore").then(({ setup }) => setup());
   });
+  whenFirstSeen("[data-edit-avatar]", () => {
+    void import("./Component/User/Avatar").then(({ setup }) => setup());
+  });
 }
diff --git a/ts/WoltLabSuite/Core/Component/User/Avatar.ts b/ts/WoltLabSuite/Core/Component/User/Avatar.ts
new file mode 100644 (file)
index 0000000..1570aac
--- /dev/null
@@ -0,0 +1,34 @@
+/**
+ * Handles the user avatar edit buttons.
+ *
+ * @author    Olaf Braun
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since     6.2
+ */
+
+import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex";
+import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector";
+import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog";
+import { close as closeControlPanel } from "WoltLabSuite/Core/Ui/User/Menu/ControlPanel";
+
+async function editAvatar(button: HTMLElement): Promise<void> {
+  // If the user is editing their own avatar, the control panel is open and can overlay the dialog.
+  closeControlPanel();
+
+  const { ok } = await dialogFactory().usingFormBuilder().fromEndpoint(button.dataset.editAvatar!);
+
+  if (ok) {
+    // TODO can we simple replace all avatar images?
+    window.location.reload();
+  }
+}
+
+export function setup(): void {
+  wheneverFirstSeen("[data-edit-avatar]", (button) => {
+    button.addEventListener(
+      "click",
+      promiseMutex(() => editAvatar(button)),
+    );
+  });
+}
index e1615e95227b2a8d157830a5c7e8624b2ab60221..143abf08defe58ad162dd8649b7a19cc198add9b 100644 (file)
@@ -44,7 +44,7 @@ function setAlignment(element: HTMLElement, referenceElement: HTMLElement): void
   }
 }
 
-function close(): void {
+export function close(): void {
   focusTrap.deactivate();
 
   element.hidden = true;
index 26090aa4b4c7d8a6767a9e96e636da4a864b7398..94c765723f709ec8d70e727dc0441a2be840a9e6 100644 (file)
@@ -99,5 +99,8 @@ define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Ui
         (0, LazyLoader_1.whenFirstSeen)("[data-ignore-user]", () => {
             void new Promise((resolve_8, reject_8) => { require(["./Component/User/Ignore"], resolve_8, reject_8); }).then(tslib_1.__importStar).then(({ setup }) => setup());
         });
+        (0, LazyLoader_1.whenFirstSeen)("[data-edit-avatar]", () => {
+            void new Promise((resolve_9, reject_9) => { require(["./Component/User/Avatar"], resolve_9, reject_9); }).then(tslib_1.__importStar).then(({ setup }) => setup());
+        });
     }
 });
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/User/Avatar.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/User/Avatar.js
new file mode 100644 (file)
index 0000000..791aa9a
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Handles the user avatar edit buttons.
+ *
+ * @author    Olaf Braun
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since     6.2
+ */
+define(["require", "exports", "WoltLabSuite/Core/Helper/PromiseMutex", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Component/Dialog", "WoltLabSuite/Core/Ui/User/Menu/ControlPanel"], function (require, exports, PromiseMutex_1, Selector_1, Dialog_1, ControlPanel_1) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+    exports.setup = setup;
+    async function editAvatar(button) {
+        // If the user is editing their own avatar, the control panel is open and can overlay the dialog.
+        (0, ControlPanel_1.close)();
+        const { ok } = await (0, Dialog_1.dialogFactory)().usingFormBuilder().fromEndpoint(button.dataset.editAvatar);
+        if (ok) {
+            // TODO can we simple replace all avatar images?
+            window.location.reload();
+        }
+    }
+    function setup() {
+        (0, Selector_1.wheneverFirstSeen)("[data-edit-avatar]", (button) => {
+            button.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(() => editAvatar(button)));
+        });
+    }
+});
index 2a3a5fc9b966317b8cdae167e98f707ae74bbc17..b0f59f31cc8ef3859863e73196b0f1ed761cdd5d 100644 (file)
@@ -9,6 +9,7 @@
 define(["require", "exports", "tslib", "../../CloseOverlay", "./Manager", "focus-trap", "../../Alignment", "../../../Dom/Util"], function (require, exports, tslib_1, CloseOverlay_1, Manager_1, focus_trap_1, Alignment, Util_1) {
     "use strict";
     Object.defineProperty(exports, "__esModule", { value: true });
+    exports.close = close;
     exports.getElement = getElement;
     exports.setup = setup;
     CloseOverlay_1 = tslib_1.__importDefault(CloseOverlay_1);
diff --git a/wcfsetup/install/files/lib/action/UserAvatarAction.class.php b/wcfsetup/install/files/lib/action/UserAvatarAction.class.php
new file mode 100644 (file)
index 0000000..502aa13
--- /dev/null
@@ -0,0 +1,148 @@
+<?php
+
+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\file\FileAction;
+use wcf\data\IStorableObject;
+use wcf\data\user\UserEditor;
+use wcf\data\user\UserProfile;
+use wcf\http\Helper;
+use wcf\system\cache\runtime\UserProfileRuntimeCache;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\file\processor\UserAvatarFileProcessor;
+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\system\user\storage\UserStorageHandler;
+use wcf\system\user\UserProfileHandler;
+use wcf\system\WCF;
+
+/**
+ * Handles user avatars editing.
+ *
+ * @author      Olaf Braun
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.2
+ */
+final class UserAvatarAction implements RequestHandlerInterface
+{
+    #[\Override]
+    public function handle(ServerRequestInterface $request): ResponseInterface
+    {
+        $parameters = Helper::mapQueryParameters(
+            $request->getQueryParams(),
+            <<<'EOT'
+                array {
+                    id?: positive-int
+                }
+                EOT
+        );
+
+        if (!WCF::getUser()->userID) {
+            throw new PermissionDeniedException();
+        }
+
+        if (isset($parameters['id'])) {
+            $user = UserProfileRuntimeCache::getInstance()->getObject($parameters['id']);
+        } else {
+            $user = UserProfileHandler::getInstance()->getUserProfile();
+        }
+
+        if (!UserAvatarFileProcessor::canEditAvatar($user)) {
+            throw new PermissionDeniedException();
+        }
+
+        $form = $this->getForm($user);
+
+        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') {
+                if ($user->avatarFileID !== null) {
+                    (new FileAction([$user->avatarFileID], 'delete'))->executeAction();
+                }
+
+                (new UserEditor($user->getDecoratedObject()))->update([
+                    'avatarFileID' => null,
+                ]);
+
+                UserStorageHandler::getInstance()->reset([$user->userID], 'avatar');
+            }
+
+            return new JsonResponse([
+                // TODO did we need the avatar url?
+                'result' => [],
+            ]);
+        } else {
+            throw new \LogicException('Unreachable');
+        }
+    }
+
+    private function getForm(UserProfile $user): Psr15DialogForm
+    {
+        $form = new Psr15DialogForm(
+            UserAvatarAction::class,
+            WCF::getLanguage()->get('wcf.user.avatarManagement')
+        );
+        $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")
+                ->required()
+                ->singleFileUpload()
+                ->bigPreview()
+                ->addDependency(
+                    ValueFormFieldDependency::create('avatarType')
+                        ->fieldId('avatarType')
+                        ->values(['custom'])
+                ),
+        ]);
+        $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);
+        $form->build();
+
+        return $form;
+    }
+}
index a2d194a1a737f09641a2ef1666cd0bf8c003bdae..f2782755f43f258408b445e7a3fefcaa13a2381e 100644 (file)
@@ -41,7 +41,7 @@ final class UserAvatarFileProcessor extends AbstractFileProcessor
         $userFromContext = $this->getUser($context);
         $userFromCoreFile = $this->getUserByFile($file);
 
-        if ($userFromContext === null) {
+        if ($userFromCoreFile === null) {
             return true;
         }
 
@@ -62,11 +62,11 @@ final class UserAvatarFileProcessor extends AbstractFileProcessor
 
         // Save the `fileID` in the session variable so that the current user can delete it the old avatar
         if ($user->avatarFileID !== null) {
-            WCF::getSession()->register(\sprintf(self::SESSION_VARIABLE, $$user->avatarFileID), TIME_NOW);
+            WCF::getSession()->register(\sprintf(self::SESSION_VARIABLE, $user->avatarFileID), TIME_NOW);
             WCF::getSession()->update();
         }
 
-        (new UserEditor($user))->update([
+        (new UserEditor($user->getDecoratedObject()))->update([
             'avatarFileID' => $file->fileID,
         ]);
         // reset user storage
@@ -82,7 +82,7 @@ final class UserAvatarFileProcessor extends AbstractFileProcessor
             return FileProcessorPreflightResult::InvalidContext;
         }
 
-        if (!$this->canEditAvatar($user)) {
+        if (!UserAvatarFileProcessor::canEditAvatar($user)) {
             return FileProcessorPreflightResult::InsufficientPermissions;
         }
 
@@ -124,7 +124,7 @@ final class UserAvatarFileProcessor extends AbstractFileProcessor
             ) !== null;
         }
 
-        return $this->canEditAvatar($user);
+        return UserAvatarFileProcessor::canEditAvatar($user);
     }
 
     #[\Override]
@@ -233,7 +233,7 @@ final class UserAvatarFileProcessor extends AbstractFileProcessor
         return UserProfileRuntimeCache::getInstance()->getObject($userID);
     }
 
-    private function canEditAvatar(UserProfile $user): bool
+    public static function canEditAvatar(UserProfile $user): bool
     {
         if (WCF::getSession()->getPermission('admin.user.canEditUser')) {
             return true;