Migrate follow/unfollow to typescript
authorMarcel Werk <burntime@woltlab.com>
Mon, 8 Jan 2024 17:53:02 +0000 (18:53 +0100)
committerMarcel Werk <burntime@woltlab.com>
Mon, 8 Jan 2024 17:53:02 +0000 (18:53 +0100)
ts/WoltLabSuite/Core/Component/User/Follow.ts [new file with mode: 0644]
wcfsetup/install/files/js/WCF.User.js
wcfsetup/install/files/js/WoltLabSuite/Core/Component/User/Follow.js [new file with mode: 0644]
wcfsetup/install/files/lib/action/UserFollowAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/user/follow/UserFollowAction.class.php
wcfsetup/install/files/lib/system/user/command/Follow.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/user/command/Unfollow.class.php [new file with mode: 0644]

diff --git a/ts/WoltLabSuite/Core/Component/User/Follow.ts b/ts/WoltLabSuite/Core/Component/User/Follow.ts
new file mode 100644 (file)
index 0000000..0bb7d09
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Handles the user follow buttons.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
+ */
+
+import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
+import { getPhrase } from "WoltLabSuite/Core/Language";
+
+function toggleFollow(button: HTMLButtonElement): void {
+  if (button.dataset.following != "1") {
+    button.dataset.following = "1";
+    button.dataset.tooltip = getPhrase("wcf.user.button.unfollow");
+    button.querySelector("fa-icon")?.setIcon("circle-minus");
+    void prepareRequest(button.dataset.endpoint!)
+      .post({
+        action: "follow",
+      })
+      .fetchAsResponse();
+  } else {
+    button.dataset.following = "0";
+    button.dataset.tooltip = getPhrase("wcf.user.button.follow");
+    button.querySelector("fa-icon")?.setIcon("circle-plus");
+    void prepareRequest(button.dataset.endpoint!)
+      .post({
+        action: "unfollow",
+      })
+      .fetchAsResponse();
+  }
+}
+
+export function setup(): void {
+  document.querySelectorAll<HTMLButtonElement>(".jsFollowButton").forEach((button) => {
+    button.addEventListener("click", () => {
+      toggleFollow(button);
+    });
+  });
+}
index 0cfaf92d9c20a1ebec452cbfe210747d79b79e64..0371d82c54a5f3c0dc46896adfcfd52ac93aab1e 100644 (file)
@@ -1151,6 +1151,8 @@ WCF.User.Action = {};
 if (COMPILER_TARGET_DEFAULT) {
        /**
         * Handles user follow and unfollow links.
+        * 
+        * @deprecated 6.1 use `WoltLabSuite/Core/Component/User/Follow` instead
         */
        WCF.User.Action.Follow = Class.extend({
                /**
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/User/Follow.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/User/Follow.js
new file mode 100644 (file)
index 0000000..7f354bb
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Handles the user follow buttons.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
+ */
+define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "WoltLabSuite/Core/Language"], function (require, exports, Backend_1, Language_1) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+    exports.setup = void 0;
+    function toggleFollow(button) {
+        if (button.dataset.following != "1") {
+            button.dataset.following = "1";
+            button.dataset.tooltip = (0, Language_1.getPhrase)("wcf.user.button.unfollow");
+            button.querySelector("fa-icon")?.setIcon("circle-minus");
+            void (0, Backend_1.prepareRequest)(button.dataset.endpoint)
+                .post({
+                action: "follow",
+            })
+                .fetchAsResponse();
+        }
+        else {
+            button.dataset.following = "0";
+            button.dataset.tooltip = (0, Language_1.getPhrase)("wcf.user.button.follow");
+            button.querySelector("fa-icon")?.setIcon("circle-plus");
+            void (0, Backend_1.prepareRequest)(button.dataset.endpoint)
+                .post({
+                action: "unfollow",
+            })
+                .fetchAsResponse();
+        }
+    }
+    function setup() {
+        document.querySelectorAll(".jsFollowButton").forEach((button) => {
+            button.addEventListener("click", () => {
+                toggleFollow(button);
+            });
+        });
+    }
+    exports.setup = setup;
+});
diff --git a/wcfsetup/install/files/lib/action/UserFollowAction.class.php b/wcfsetup/install/files/lib/action/UserFollowAction.class.php
new file mode 100644 (file)
index 0000000..959fa93
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+
+namespace wcf\action;
+
+use Laminas\Diactoros\Response\EmptyResponse;
+use Laminas\Diactoros\Response\TextResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use wcf\data\user\User;
+use wcf\http\Helper;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\user\command\Follow;
+use wcf\system\user\command\Unfollow;
+use wcf\system\WCF;
+
+/**
+ * Handles user follows.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2023 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+final class UserFollowAction implements RequestHandlerInterface
+{
+    /**
+     * @inheritDoc
+     */
+    public function handle(ServerRequestInterface $request): ResponseInterface
+    {
+        $parameters = Helper::mapQueryParameters(
+            $request->getQueryParams(),
+            <<<'EOT'
+                array {
+                    id: positive-int
+                }
+                EOT
+        );
+
+        $this->assertUserIsLoggedIn();
+
+        $user = new User($parameters['id']);
+        $this->assertTargetCanBeFollowed($user);
+
+        if ($request->getMethod() === 'GET') {
+            return new TextResponse('Unsupported', 400);
+        } elseif ($request->getMethod() === 'POST') {
+            $bodyParameters = Helper::mapRequestBody(
+                $request->getParsedBody(),
+                <<<'EOT'
+                    array {
+                        action: "follow" | "unfollow"
+                    }
+                    EOT
+            );
+
+            if ($bodyParameters['action'] == 'follow') {
+                $this->assertUserIsNotIgnored($user);
+
+                $command = new Follow(WCF::getUser(), $user);
+                $command();
+            } else {
+                $command = new Unfollow(WCF::getUser(), $user);
+                $command();
+            }
+
+            return new EmptyResponse();
+        } else {
+            throw new \LogicException('Unreachable');
+        }
+    }
+
+    private function assertUserIsLoggedIn(): void
+    {
+        if (!WCF::getUser()->userID) {
+            throw new PermissionDeniedException();
+        }
+    }
+
+    private function assertUserIsNotIgnored(User $target): void
+    {
+        $sql = "SELECT  ignoreID
+                FROM    wcf" . WCF_N . "_user_ignore
+                WHERE   userID = ?
+                    AND ignoreUserID = ?";
+        $statement = WCF::getDB()->prepareStatement($sql);
+        $statement->execute([
+            $target->userID,
+            WCF::getUser()->userID,
+        ]);
+
+        $ignoreID = $statement->fetchSingleColumn();
+        if ($ignoreID !== false) {
+            throw new PermissionDeniedException();
+        }
+    }
+
+    private function assertTargetCanBeFollowed(User $target): void
+    {
+        if (!$target->userID) {
+            throw new IllegalLinkException();
+        }
+
+        if ($target->userID === WCF::getUser()->userID) {
+            throw new IllegalLinkException();
+        }
+    }
+}
index 87f510b746741a7ae0887d4c993b3c2bc4b04d42..a14e7af2b893c1d740c0e879d188c7d66837deb8 100644 (file)
@@ -4,14 +4,14 @@ namespace wcf\data\user\follow;
 
 use wcf\data\AbstractDatabaseObjectAction;
 use wcf\data\IGroupedUserListAction;
-use wcf\data\user\UserProfile;
+use wcf\data\user\User;
 use wcf\system\cache\runtime\UserProfileRuntimeCache;
 use wcf\system\exception\PermissionDeniedException;
 use wcf\system\exception\UserInputException;
 use wcf\system\user\activity\event\UserActivityEventHandler;
+use wcf\system\user\command\Follow;
+use wcf\system\user\command\Unfollow;
 use wcf\system\user\GroupedUserList;
-use wcf\system\user\notification\object\UserFollowUserNotificationObject;
-use wcf\system\user\notification\UserNotificationHandler;
 use wcf\system\user\storage\UserStorageHandler;
 use wcf\system\WCF;
 
@@ -41,6 +41,8 @@ class UserFollowAction extends AbstractDatabaseObjectAction implements IGroupedU
 
     /**
      * Validates given parameters.
+     *
+     * @deprecated 6.1 use `wcf\action\UserFollowAction` instead
      */
     public function validateFollow()
     {
@@ -71,35 +73,12 @@ class UserFollowAction extends AbstractDatabaseObjectAction implements IGroupedU
      * Follows a user.
      *
      * @return  array
+     * @deprecated 6.1 use `wcf\action\UserFollowAction` instead
      */
     public function follow()
     {
-        /** @var UserFollow $follow */
-        $follow = UserFollowEditor::createOrIgnore([
-            'userID' => WCF::getUser()->userID,
-            'followUserID' => $this->parameters['data']['userID'],
-            'time' => TIME_NOW,
-        ]);
-
-        if ($follow !== null) {
-            // send notification
-            UserNotificationHandler::getInstance()->fireEvent(
-                'following',
-                'com.woltlab.wcf.user.follow',
-                new UserFollowUserNotificationObject($follow),
-                [$follow->followUserID]
-            );
-
-            // fire activity event
-            UserActivityEventHandler::getInstance()->fireEvent(
-                'com.woltlab.wcf.user.recentActivityEvent.follow',
-                $this->parameters['data']['userID']
-            );
-
-            // reset storage
-            UserStorageHandler::getInstance()->reset([$this->parameters['data']['userID']], 'followerUserIDs');
-            UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'followingUserIDs');
-        }
+        $command = new Follow(WCF::getUser(), new User($this->parameters['data']['userID']));
+        $command();
 
         return [
             'following' => 1,
@@ -108,6 +87,7 @@ class UserFollowAction extends AbstractDatabaseObjectAction implements IGroupedU
 
     /**
      * @inheritDoc
+     * @deprecated 6.1 use `wcf\action\UserFollowAction` instead
      */
     public function validateUnfollow()
     {
@@ -118,25 +98,12 @@ class UserFollowAction extends AbstractDatabaseObjectAction implements IGroupedU
      * Stops following a user.
      *
      * @return  array
+     * @deprecated 6.1 use `wcf\action\UserFollowAction` instead
      */
     public function unfollow()
     {
-        $follow = UserFollow::getFollow(WCF::getUser()->userID, $this->parameters['data']['userID']);
-
-        if ($follow->followID) {
-            $followEditor = new UserFollowEditor($follow);
-            $followEditor->delete();
-
-            // remove activity event
-            UserActivityEventHandler::getInstance()->removeEvent(
-                'com.woltlab.wcf.user.recentActivityEvent.follow',
-                $this->parameters['data']['userID']
-            );
-        }
-
-        // reset storage
-        UserStorageHandler::getInstance()->reset([$this->parameters['data']['userID']], 'followerUserIDs');
-        UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'followingUserIDs');
+        $command = new Unfollow(WCF::getUser(), new User($this->parameters['data']['userID']));
+        $command();
 
         return [
             'following' => 0,
diff --git a/wcfsetup/install/files/lib/system/user/command/Follow.class.php b/wcfsetup/install/files/lib/system/user/command/Follow.class.php
new file mode 100644 (file)
index 0000000..7aecb2b
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+namespace wcf\system\user\command;
+
+use wcf\data\user\follow\UserFollow;
+use wcf\data\user\follow\UserFollowEditor;
+use wcf\data\user\User;
+use wcf\system\user\activity\event\UserActivityEventHandler;
+use wcf\system\user\notification\object\UserFollowUserNotificationObject;
+use wcf\system\user\notification\UserNotificationHandler;
+use wcf\system\user\storage\UserStorageHandler;
+
+/**
+ * Saves that a user is following another user.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+final class Follow
+{
+    public function __construct(private readonly User $user, private readonly User $target)
+    {
+    }
+
+    public function __invoke(): void
+    {
+        $follow = UserFollowEditor::createOrIgnore([
+            'userID' => $this->user->userID,
+            'followUserID' => $this->target->userID,
+            'time' => TIME_NOW,
+        ]);
+
+        if ($follow === null) {
+            return;
+        }
+
+        \assert($follow instanceof UserFollow);
+        $this->sendNotification($follow);
+        $this->fireActivityEvent();
+        $this->resetUserStorage();
+    }
+
+    private function sendNotification(UserFollow $follow): void
+    {
+        UserNotificationHandler::getInstance()->fireEvent(
+            'following',
+            'com.woltlab.wcf.user.follow',
+            new UserFollowUserNotificationObject($follow),
+            [$follow->followUserID]
+        );
+    }
+
+    private function fireActivityEvent(): void
+    {
+        UserActivityEventHandler::getInstance()->fireEvent(
+            'com.woltlab.wcf.user.recentActivityEvent.follow',
+            $this->target->userID
+        );
+    }
+
+    private function resetUserStorage(): void
+    {
+        UserStorageHandler::getInstance()->reset([$this->target->userID], 'followerUserIDs');
+        UserStorageHandler::getInstance()->reset([$this->user->userID], 'followingUserIDs');
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/user/command/Unfollow.class.php b/wcfsetup/install/files/lib/system/user/command/Unfollow.class.php
new file mode 100644 (file)
index 0000000..0c01f98
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+
+namespace wcf\system\user\command;
+
+use wcf\data\user\follow\UserFollow;
+use wcf\data\user\follow\UserFollowEditor;
+use wcf\data\user\User;
+use wcf\system\user\activity\event\UserActivityEventHandler;
+use wcf\system\user\storage\UserStorageHandler;
+
+/**
+ * Saves that a user is unfollowing another user.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+final class Unfollow
+{
+    public function __construct(private readonly User $user, private readonly User $target)
+    {
+    }
+
+    public function __invoke(): void
+    {
+        $follow = UserFollow::getFollow($this->user->userID, $this->target->userID);
+
+        if ($follow->followID) {
+            $followEditor = new UserFollowEditor($follow);
+            $followEditor->delete();
+
+            $this->removeActivityEvent();
+        }
+
+        $this->resetUserStorage();
+    }
+
+    private function removeActivityEvent(): void
+    {
+        UserActivityEventHandler::getInstance()->removeEvent(
+            'com.woltlab.wcf.user.recentActivityEvent.follow',
+            $this->target->userID
+        );
+    }
+
+    private function resetUserStorage(): void
+    {
+        UserStorageHandler::getInstance()->reset([$this->target->userID], 'followerUserIDs');
+        UserStorageHandler::getInstance()->reset([$this->user->userID], 'followingUserIDs');
+    }
+}