From e51e889ab5f5c7da984f5da7cfe5af05522bba75 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 7 Aug 2024 13:40:16 +0100 Subject: [PATCH 01/11] chore(2.0): code formatting with prettier Start by formatting your code with Prettier. --- js/src/forum/utils/groupBy.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/js/src/forum/utils/groupBy.ts b/js/src/forum/utils/groupBy.ts index 62c8c41..f302d77 100644 --- a/js/src/forum/utils/groupBy.ts +++ b/js/src/forum/utils/groupBy.ts @@ -1,9 +1,12 @@ export type GroupByFunction = (val: T) => string | number; -const groupBy = (arr: T[], fn: GroupByFunction | keyof T): Record => - arr.map(typeof fn === 'function' ? fn : (val) => (val as any)[fn]).reduce((acc, val, i) => { - acc[val] = (acc[val] || []).concat(arr[i]); - return acc; - }, {} as Record); +const groupBy = (arr: T[], fn: GroupByFunction | keyof T): Record => + arr.map(typeof fn === 'function' ? fn : (val) => (val as any)[fn]).reduce( + (acc, val, i) => { + acc[val] = (acc[val] || []).concat(arr[i]); + return acc; + }, + {} as Record + ); export default groupBy; From 4c7dd591ffaaf1fc10cd10b903a5d14d0c162088 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 7 Aug 2024 13:41:16 +0100 Subject: [PATCH 02/11] chore(2.0): update dependencies Update dependencies to Flarum 2.0 compatible versions. --- composer.json | 5 ++--- js/package.json | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 45a83f3..64c7794 100755 --- a/composer.json +++ b/composer.json @@ -23,8 +23,7 @@ } ], "require": { - "php": "^7.4 | ^8.0", - "flarum/core": "^1.2.0" + "flarum/core": "^2.0.0" }, "replace": { "reflar/reactions": "*" @@ -92,7 +91,7 @@ "analyse:phpstan": "Run static analysis" }, "require-dev": { - "flarum/testing": "^1.0.0", + "flarum/testing": "^2.0.0", "flarum/likes": "*", "fof/gamification": "*", "flarum/phpstan": "*" diff --git a/js/package.json b/js/package.json index 05eb503..27bb14b 100644 --- a/js/package.json +++ b/js/package.json @@ -12,7 +12,7 @@ "dependencies": { "@flarum/prettier-config": "^1.0.0", "flarum-tsconfig": "^1.0.2", - "flarum-webpack-config": "^2.0.0", + "flarum-webpack-config": "^3.0.0", "fuzzyset": "^1.0.7", "lodash.debounce": "^4.0.8", "simple-emoji-map": "^0.5.1", @@ -22,4 +22,4 @@ "devDependencies": { "prettier": "^2.8.8" } -} +} \ No newline at end of file From f60e168c0b119795449fbde9b926c172f6f0143f Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 7 Aug 2024 13:41:40 +0100 Subject: [PATCH 03/11] chore(2.0): update infrastructure Update the extension infrastructure --- .github/workflows/backend.yml | 3 +-- .github/workflows/frontend.yml | 2 +- phpstan.neon | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 1dce7ce..5ece82b 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -4,9 +4,8 @@ on: [workflow_dispatch, push, pull_request] jobs: run: - uses: flarum/framework/.github/workflows/REUSABLE_backend.yml@main + uses: flarum/framework/.github/workflows/REUSABLE_backend.yml@2.x with: enable_backend_testing: true enable_phpstan: true backend_directory: . - php_versions: '["7.4", "8.0", "8.1", "8.2", "8.3"]' diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 2f78a17..cfb4dfb 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -4,7 +4,7 @@ on: [workflow_dispatch, push, pull_request] jobs: run: - uses: flarum/framework/.github/workflows/REUSABLE_frontend.yml@main + uses: flarum/framework/.github/workflows/REUSABLE_frontend.yml@2.x with: enable_bundlewatch: false enable_prettier: true diff --git a/phpstan.neon b/phpstan.neon index bff5e7d..f9bf9f5 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,6 +9,5 @@ parameters: - extend.php excludePaths: - *.blade.php - checkMissingIterableValueType: false databaseMigrationsPath: ['migrations'] \ No newline at end of file From de9b71a2ccab32a583d93291ca3ae8cd92a210d4 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 7 Aug 2024 13:42:07 +0100 Subject: [PATCH 04/11] chore(2.0): adapt to extending lazy modules Some Flarum modules are now lazy loaded. Extending them requires a different approach. --- js/src/forum/components/PostReactAction.js | 3 +-- js/src/forum/index.js | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/js/src/forum/components/PostReactAction.js b/js/src/forum/components/PostReactAction.js index b96f3a3..82a2caf 100755 --- a/js/src/forum/components/PostReactAction.js +++ b/js/src/forum/components/PostReactAction.js @@ -3,7 +3,6 @@ import Component from 'flarum/common/Component'; import ItemList from 'flarum/common/utils/ItemList'; import Button from 'flarum/common/components/Button'; import listItems from 'flarum/common/helpers/listItems'; -import LogInModal from 'flarum/forum/components/LogInModal'; import ReactionComponent from '../../common/components/ReactionComponent'; export default class PostReactAction extends Component { @@ -148,7 +147,7 @@ export default class PostReactAction extends Component { const allowAnonymous = app.forum.attribute('fofReactionsAllowAnonymous'); if (!app.session.user && !allowAnonymous) { - app.modal.show(LogInModal); + app.modal.show(() => import('flarum/forum/components/LogInModal')); return; } diff --git a/js/src/forum/index.js b/js/src/forum/index.js index eb304ab..01fffe1 100755 --- a/js/src/forum/index.js +++ b/js/src/forum/index.js @@ -4,7 +4,6 @@ import Forum from 'flarum/common/models/Forum'; import Discussion from 'flarum/common/models/Discussion'; import Post from 'flarum/common/models/Post'; import Model from 'flarum/common/Model'; -import NotificationGrid from 'flarum/forum/components/NotificationGrid'; import PostReactedNotification from './components/PostReactedNotification'; import Reaction from '../common/models/Reaction'; @@ -38,7 +37,7 @@ app.initializers.add('fof/reactions', () => { addReactionAction(); addPusher(); - extend(NotificationGrid.prototype, 'notificationTypes', (items) => { + extend('flarum/forum/components/NotificationGrid', 'notificationTypes', (items) => { items.add('postReacted', { name: 'postReacted', icon: 'far fa-smile', From c3d370b79f6989acdb364d195a95a8f38c1b31a7 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 7 Aug 2024 13:42:25 +0100 Subject: [PATCH 05/11] chore(2.0): misc frontend changes Miscellaneous frontend changes --- js/src/forum/components/PostReactedNotification.js | 4 ++-- js/src/forum/components/ReactionsModal.tsx | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/js/src/forum/components/PostReactedNotification.js b/js/src/forum/components/PostReactedNotification.js index 4d312ef..fa4b7f3 100644 --- a/js/src/forum/components/PostReactedNotification.js +++ b/js/src/forum/components/PostReactedNotification.js @@ -1,6 +1,6 @@ import app from 'flarum/forum/app'; import Notification from 'flarum/forum/components/Notification'; -import icon from 'flarum/common/helpers/icon'; +import Icon from 'flarum/common/components/Icon'; import emoji from '../../common/util/emoji'; @@ -18,7 +18,7 @@ export default class PostReactedNotification extends Notification { const { identifier, type } = JSON.parse(notification.content()); const user = notification.fromUser(); - const reaction = type === 'emoji' ? : icon(identifier); + const reaction = type === 'emoji' ? : ; return app.translator.trans('fof-reactions.forum.notification', { user, diff --git a/js/src/forum/components/ReactionsModal.tsx b/js/src/forum/components/ReactionsModal.tsx index 25b368b..7e64d0e 100644 --- a/js/src/forum/components/ReactionsModal.tsx +++ b/js/src/forum/components/ReactionsModal.tsx @@ -1,7 +1,7 @@ import app from 'flarum/forum/app'; import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal'; import LoadingIndicator from 'flarum/common/components/LoadingIndicator'; -import avatar from 'flarum/common/helpers/avatar'; +import Avatar from 'flarum/common/components/Avatar'; import username from 'flarum/common/helpers/username'; import Link from 'flarum/common/components/Link'; import ReactionComponent from '../../common/components/ReactionComponent'; @@ -85,13 +85,11 @@ export default class ReactionsModal extends Modal { /> )} -
- {Object.entries(users).map(([postReactionId, user]: [string, User], index: number) => (
  • - {avatar(user, { loading: 'lazy' })} + {username(user)} {canDeleteReaction(user) && ( @@ -104,7 +102,6 @@ export default class ReactionsModal extends Modal { )}
  • ))} - {anonymousCount > 0 &&
  • {app.translator.trans('fof-reactions.forum.modal.anonymous_count', { count: anonymousCount })}
  • } ); From 989d658874db43c93fc6290e95a4a175f2b9f849 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 7 Aug 2024 13:43:48 +0100 Subject: [PATCH 06/11] chore(2.0): misc backend changes Miscellaneous backend changes --- extend.php | 2 +- src/Notification/PostReactedBlueprint.php | 11 ++++++----- src/PostReaction.php | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/extend.php b/extend.php index ef85134..74c231a 100644 --- a/extend.php +++ b/extend.php @@ -49,7 +49,7 @@ ->subscribe(Listener\SendNotifications::class), (new Extend\Notification()) - ->type(PostReactedBlueprint::class, BasicPostSerializer::class, ['alert']), + ->type(PostReactedBlueprint::class, ['alert']), (new Extend\ApiSerializer(Serializer\ForumSerializer::class)) ->hasMany('reactions', ReactionSerializer::class) diff --git a/src/Notification/PostReactedBlueprint.php b/src/Notification/PostReactedBlueprint.php index 1b817ea..b41f8e5 100755 --- a/src/Notification/PostReactedBlueprint.php +++ b/src/Notification/PostReactedBlueprint.php @@ -11,6 +11,7 @@ namespace FoF\Reactions\Notification; +use Flarum\Database\AbstractModel; use Flarum\Notification\Blueprint\BlueprintInterface; use Flarum\Post\Post; use Flarum\User\User; @@ -46,7 +47,7 @@ public function __construct(Post $post, User $user, string $reaction) /** * {@inheritdoc} */ - public static function getType() + public static function getType(): string { return 'postReacted'; } @@ -54,7 +55,7 @@ public static function getType() /** * {@inheritdoc} */ - public static function getSubjectModel() + public static function getSubjectModel(): string { return Post::class; } @@ -62,7 +63,7 @@ public static function getSubjectModel() /** * {@inheritdoc} */ - public function getSubject() + public function getSubject(): ?AbstractModel { return $this->post; } @@ -70,7 +71,7 @@ public function getSubject() /** * {@inheritdoc} */ - public function getFromUser() + public function getFromUser(): ?User { return $this->user; } @@ -88,7 +89,7 @@ public function getReactionType() /** * {@inheritdoc} */ - public function getData() + public function getData(): mixed { return $this->reaction; } diff --git a/src/PostReaction.php b/src/PostReaction.php index 21feffb..bb5026e 100644 --- a/src/PostReaction.php +++ b/src/PostReaction.php @@ -33,7 +33,7 @@ class PostReaction extends AbstractModel public $timestamps = true; - public $dates = ['created_at', 'updated_at']; + protected $casts = ['created_at' => 'datetime', 'updated_at' => 'datetime']; public function reaction() { From 017be166a2ccf0e25426441b0b0a95567dc53e03 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 7 Aug 2024 17:23:46 +0100 Subject: [PATCH 07/11] chore(2.0): JSON:API changes Flarum 2.0 completely changes the JSON:API implementation --- extend.php | 56 ++++---- js/src/forum/components/ReactionsModal.tsx | 2 +- src/Access/ScopePostReactionVisibility.php | 16 +++ .../Controller/CreateReactionController.php | 43 ------ .../DeletePostReactionController.php | 1 + .../Controller/DeleteReactionController.php | 45 ------- .../ListPostReactionsController.php | 77 ----------- .../Controller/ListReactionsController.php | 37 ------ .../Controller/UpdateReactionController.php | 56 -------- src/Api/Resource/PostReactionResource.php | 65 +++++++++ src/Api/Resource/ReactionResource.php | 123 ++++++++++++++++++ src/Api/Serializer/PostReactionSerializer.php | 1 + src/Api/Serializer/ReactionSerializer.php | 35 ----- src/Command/CreateReaction.php | 41 ------ src/Command/CreateReactionHandler.php | 73 ----------- src/Command/DeleteReaction.php | 52 -------- src/Command/DeleteReactionHandler.php | 56 -------- src/Command/EditReaction.php | 50 ------- src/Command/EditReactionHandler.php | 73 ----------- src/ForumResourceFields.php | 38 ++++++ src/PostResourceEndpoints.php | 63 +++++++++ ...tAttributes.php => PostResourceFields.php} | 42 +++--- src/ReactionsForumAttributes.php | 39 ------ src/Search/Filter/PostFilter.php | 39 ++++++ src/Search/PostReactionSearcher.php | 40 ++++++ src/Validator/ReactionValidator.php | 38 ------ 26 files changed, 430 insertions(+), 771 deletions(-) create mode 100644 src/Access/ScopePostReactionVisibility.php delete mode 100644 src/Api/Controller/CreateReactionController.php delete mode 100644 src/Api/Controller/DeleteReactionController.php delete mode 100644 src/Api/Controller/ListPostReactionsController.php delete mode 100644 src/Api/Controller/ListReactionsController.php delete mode 100644 src/Api/Controller/UpdateReactionController.php create mode 100644 src/Api/Resource/PostReactionResource.php create mode 100644 src/Api/Resource/ReactionResource.php delete mode 100644 src/Api/Serializer/ReactionSerializer.php delete mode 100644 src/Command/CreateReaction.php delete mode 100644 src/Command/CreateReactionHandler.php delete mode 100644 src/Command/DeleteReaction.php delete mode 100644 src/Command/DeleteReactionHandler.php delete mode 100644 src/Command/EditReaction.php delete mode 100644 src/Command/EditReactionHandler.php create mode 100644 src/ForumResourceFields.php create mode 100644 src/PostResourceEndpoints.php rename src/{PostAttributes.php => PostResourceFields.php} (68%) delete mode 100644 src/ReactionsForumAttributes.php create mode 100644 src/Search/Filter/PostFilter.php create mode 100644 src/Search/PostReactionSearcher.php delete mode 100644 src/Validator/ReactionValidator.php diff --git a/extend.php b/extend.php index 74c231a..72af3ba 100644 --- a/extend.php +++ b/extend.php @@ -11,16 +11,18 @@ namespace FoF\Reactions; -use Flarum\Api\Controller as ApiController; -use Flarum\Api\Serializer; -use Flarum\Api\Serializer\BasicPostSerializer; -use Flarum\Database\AbstractModel; +use Flarum\Api\Context; +use Flarum\Discussion\Discussion; use Flarum\Extend; use Flarum\Post\Event\Saving; use Flarum\Post\Post; -use FoF\Reactions\Api\Controller; -use FoF\Reactions\Api\Serializer\ReactionSerializer; +use Flarum\Search\Database\DatabaseSearchDriver; use FoF\Reactions\Notification\PostReactedBlueprint; +use Flarum\Api\Resource; +use Flarum\Api\Endpoint; +use Flarum\Api\Schema; +use FoF\Reactions\Search\Filter\PostFilter; +use FoF\Reactions\Search\PostReactionSearcher; return [ (new Extend\Frontend('admin')) @@ -35,14 +37,8 @@ new Extend\Locales(__DIR__.'/resources/locale'), - (new Extend\Routes('api')) - ->get('/posts/{id}/reactions', 'post.reactions.index', Controller\ListPostReactionsController::class) - ->delete('/posts/{id}/reactions/specific/{postReactionId}', 'post.reactions.specific.delete', Controller\DeletePostReactionController::class) - ->delete('/posts/{id}/reactions/type/{reactionId}', 'post.reactions.type.delete', Controller\DeletePostReactionController::class) - ->get('/reactions', 'reactions.index', Controller\ListReactionsController::class) - ->post('/reactions', 'reactions.create', Controller\CreateReactionController::class) - ->patch('/reactions/{id}', 'reactions.update', Controller\UpdateReactionController::class) - ->delete('/reactions/{id}', 'reactions.delete', Controller\DeleteReactionController::class), + (new Extend\ModelVisibility(PostReaction::class)) + ->scope(Access\ScopePostReactionVisibility::class), (new Extend\Event()) ->listen(Saving::class, Listener\SaveReactionsToDatabase::class) @@ -51,27 +47,27 @@ (new Extend\Notification()) ->type(PostReactedBlueprint::class, ['alert']), - (new Extend\ApiSerializer(Serializer\ForumSerializer::class)) - ->hasMany('reactions', ReactionSerializer::class) - ->attributes(ReactionsForumAttributes::class), + new Extend\ApiResource(Api\Resource\PostReactionResource::class), - (new Extend\ApiSerializer(Serializer\PostSerializer::class)) - ->attributes(PostAttributes::class), + new Extend\ApiResource(Api\Resource\ReactionResource::class), - (new Extend\ApiSerializer(Serializer\DiscussionSerializer::class)) - ->attributes(function (Serializer\DiscussionSerializer $serializer, AbstractModel $discussion, array $attributes): array { - $attributes['canSeeReactions'] = (bool) $serializer->getActor()->can('canSeeReactions', $discussion); + (new Extend\ApiResource(Resource\ForumResource::class)) + ->fields(ForumResourceFields::class) + ->endpoint(Endpoint\Show::class, fn (Endpoint\Show $show) => $show->addDefaultInclude(['reactions'])), - return $attributes; - }), + (new Extend\ApiResource(Resource\PostResource::class)) + ->fields(PostResourceFields::class) + ->endpoints(PostRso), - (new Extend\ApiController(ApiController\ShowForumController::class)) - ->prepareDataForSerialization(function (ApiController\ShowForumController $controller, &$data) { - $data['reactions'] = Reaction::get(); - }), + (new Extend\ApiResource(Resource\DiscussionResource::class)) + ->fields(fn (): array => [ + Schema\Boolean::make('canSeeReactions') + ->get(fn (Discussion $discussion, Context $context) => $context->getActor()->can('canSeeReactions', $discussion)) + ]), - (new Extend\ApiController(ApiController\ShowForumController::class)) - ->addInclude('reactions'), + (new Extend\SearchDriver(DatabaseSearchDriver::class)) + ->addSearcher(PostReaction::class, PostReactionSearcher::class) + ->addFilter(PostReactionSearcher::class, PostFilter::class), (new Extend\Settings()) ->default('fof-reactions.react_own_post', false) diff --git a/js/src/forum/components/ReactionsModal.tsx b/js/src/forum/components/ReactionsModal.tsx index 7e64d0e..56d9369 100644 --- a/js/src/forum/components/ReactionsModal.tsx +++ b/js/src/forum/components/ReactionsModal.tsx @@ -110,7 +110,7 @@ export default class ReactionsModal extends Modal { async load(): Promise { this.loading = true; - const response = await app.store.find>(`/posts/${this.attrs.post.id()}/reactions`, { include: 'user,reaction' }); + const response = await app.store.find>(`/posts_reactions`, { include: 'user,reaction', filter: { post: this.attrs.post.id() } }); const groupedReactions = groupBy(response, (r: PostReaction) => r.reactionId()); const reactions: ReactionGroup[] = []; diff --git a/src/Access/ScopePostReactionVisibility.php b/src/Access/ScopePostReactionVisibility.php new file mode 100644 index 0000000..60535ab --- /dev/null +++ b/src/Access/ScopePostReactionVisibility.php @@ -0,0 +1,16 @@ +whereHas('post', function (Builder $query) use ($actor) { + $query->whereVisibleTo($actor); + }); + } +} diff --git a/src/Api/Controller/CreateReactionController.php b/src/Api/Controller/CreateReactionController.php deleted file mode 100644 index 5a21e49..0000000 --- a/src/Api/Controller/CreateReactionController.php +++ /dev/null @@ -1,43 +0,0 @@ -bus = $bus; - } - - protected function data(ServerRequestInterface $request, Document $document) - { - return $this->bus->dispatch( - new CreateReaction(RequestUtil::getActor($request), Arr::get($request->getParsedBody(), 'data.attributes', $request->getParsedBody())) - ); - } -} diff --git a/src/Api/Controller/DeletePostReactionController.php b/src/Api/Controller/DeletePostReactionController.php index 112e366..5575d8b 100644 --- a/src/Api/Controller/DeletePostReactionController.php +++ b/src/Api/Controller/DeletePostReactionController.php @@ -21,6 +21,7 @@ use Laminas\Diactoros\Response\EmptyResponse; use Psr\Http\Message\ServerRequestInterface; +/** @TODO: Remove this in favor of the api resource class that was added. */ class DeletePostReactionController extends AbstractDeleteController { /** diff --git a/src/Api/Controller/DeleteReactionController.php b/src/Api/Controller/DeleteReactionController.php deleted file mode 100644 index c2c3351..0000000 --- a/src/Api/Controller/DeleteReactionController.php +++ /dev/null @@ -1,45 +0,0 @@ -bus = $bus; - } - - /** - * @param ServerRequestInterface $request - */ - protected function delete(ServerRequestInterface $request) - { - $this->bus->dispatch( - new DeleteReaction(Arr::get($request->getQueryParams(), 'id'), RequestUtil::getActor($request)) - ); - } -} diff --git a/src/Api/Controller/ListPostReactionsController.php b/src/Api/Controller/ListPostReactionsController.php deleted file mode 100644 index ea6b5f2..0000000 --- a/src/Api/Controller/ListPostReactionsController.php +++ /dev/null @@ -1,77 +0,0 @@ -posts = $posts; - $this->settings = $settings; - } - - /** - * @param ServerRequestInterface $request - * @param Document $document - * - * @return mixed - */ - protected function data(ServerRequestInterface $request, Document $document) - { - $actor = RequestUtil::getActor($request); - - $postId = Arr::get($request->getQueryParams(), 'id'); - $post = $this->posts->findOrFail($postId, $actor); - - $actor->assertCan('canSeeReactions', $post->discussion); - - $query = PostReaction::query() - ->where('post_id', $post->id) - ->whereNotNull('reaction_id'); - - if ($this->settings->get('fof-reactions.anonymousReactions')) { - $query->unionAll( - PostAnonymousReaction::query()->where('post_id', $post->id) - ->whereNotNull('reaction_id')->toBase() - ); - } - - return $query->get(); - } -} diff --git a/src/Api/Controller/ListReactionsController.php b/src/Api/Controller/ListReactionsController.php deleted file mode 100644 index 0451346..0000000 --- a/src/Api/Controller/ListReactionsController.php +++ /dev/null @@ -1,37 +0,0 @@ -bus = $bus; - } - - /** - * @param ServerRequestInterface $request - * @param Document $document - * - * @return mixed - */ - protected function data(ServerRequestInterface $request, Document $document) - { - $id = Arr::get($request->getQueryParams(), 'id'); - $actor = RequestUtil::getActor($request); - $data = Arr::get($request->getParsedBody(), 'attributes', []); - - return $this->bus->dispatch( - new EditReaction($id, $actor, $data) - ); - } -} diff --git a/src/Api/Resource/PostReactionResource.php b/src/Api/Resource/PostReactionResource.php new file mode 100644 index 0000000..668b15e --- /dev/null +++ b/src/Api/Resource/PostReactionResource.php @@ -0,0 +1,65 @@ + + */ +class PostReactionResource extends Resource\AbstractDatabaseResource +{ + public function type(): string + { + return 'post_reactions'; + } + + public function model(): string + { + return PostReaction::class; + } + + public function endpoints(): array + { + return [ + Endpoint\Delete::make() + ->can('delete'), + Endpoint\Index::make() + ->paginate(), + ]; + } + + public function fields(): array + { + return [ + + Schema\Integer::make('userId'), + Schema\Integer::make('postId'), + Schema\Integer::make('reactionId'), + + Schema\Relationship\ToOne::make('reaction') + ->includable() + ->type('reactions'), + Schema\Relationship\ToOne::make('user') + ->includable() + ->type('users'), + Schema\Relationship\ToOne::make('post') + ->includable() + ->type('posts'), + ]; + } + + public function sorts(): array + { + return [ + // SortColumn::make('createdAt'), + ]; + } +} diff --git a/src/Api/Resource/ReactionResource.php b/src/Api/Resource/ReactionResource.php new file mode 100644 index 0000000..56f8e9e --- /dev/null +++ b/src/Api/Resource/ReactionResource.php @@ -0,0 +1,123 @@ + + */ +class ReactionResource extends Resource\AbstractDatabaseResource +{ + public function type(): string + { + return 'reactions'; + } + + public function model(): string + { + return Reaction::class; + } + + public function scope(Builder $query, OriginalContext $context): void + { + $query->whereVisibleTo($context->getActor()); + } + + public function endpoints(): array + { + return [ + Endpoint\Create::make() + ->admin(), + Endpoint\Update::make() + ->admin(), + Endpoint\Delete::make() + ->admin(), + Endpoint\Index::make(), + ]; + } + + public function fields(): array + { + return [ + + Schema\Str::make('identifier') + ->requiredOnCreate() + ->maxLength(255) + ->unique('reactions', 'identifier') + ->writable(), + Schema\Str::make('display') + ->maxLength(255) + ->nullable() + ->writableOnUpdate(), + Schema\Str::make('type') + ->requiredOnCreate() + ->maxLength(255) + ->regex('/icon|emoji/i') + ->writable(), + Schema\Boolean::make('enabled') + ->nullable() + ->writable(), + + ]; + } + + public function sorts(): array + { + return [ + // SortColumn::make('createdAt'), + ]; + } + + /** + * @inheritDoc + */ + public function creating(object $model, OriginalContext $context): ?object + { + $this->events->dispatch(new Creating($model, $context->getActor(), Arr::get($context->body(), 'data.attributes', $context->body()))); + + return parent::creating($model, $context); + } + + /** + * @inheritDoc + */ + public function created(object $model, OriginalContext $context): ?object + { + $this->events->dispatch(new Created($model, $context->getActor())); + + return parent::created($model, $context); + } + + /** + * @inheritDoc + */ + public function deleting(object $model, OriginalContext $context): void + { + $this->events->dispatch(new Deleting($model, $context->getActor())); + + parent::deleting($model, $context); + } + + /** + * @inheritDoc + */ + public function deleted(object $model, OriginalContext $context): void + { + $this->events->dispatch(new Deleted($model, $context->getActor())); + + parent::deleted($model, $context); + } +} diff --git a/src/Api/Serializer/PostReactionSerializer.php b/src/Api/Serializer/PostReactionSerializer.php index e587365..790ed0d 100644 --- a/src/Api/Serializer/PostReactionSerializer.php +++ b/src/Api/Serializer/PostReactionSerializer.php @@ -17,6 +17,7 @@ use FoF\Reactions\PostReaction; use Illuminate\Support\Str; +/** @TODO: Remove this in favor of the api resource class that was added. */ class PostReactionSerializer extends AbstractSerializer { protected $type = 'post_reactions'; diff --git a/src/Api/Serializer/ReactionSerializer.php b/src/Api/Serializer/ReactionSerializer.php deleted file mode 100644 index 1f17c5c..0000000 --- a/src/Api/Serializer/ReactionSerializer.php +++ /dev/null @@ -1,35 +0,0 @@ - $reaction->identifier, - 'display' => $reaction->display, - 'type' => $reaction->type, - 'enabled' => $reaction->enabled, - ]; - } -} diff --git a/src/Command/CreateReaction.php b/src/Command/CreateReaction.php deleted file mode 100644 index 9013d54..0000000 --- a/src/Command/CreateReaction.php +++ /dev/null @@ -1,41 +0,0 @@ -actor = $actor; - $this->data = $data; - } -} diff --git a/src/Command/CreateReactionHandler.php b/src/Command/CreateReactionHandler.php deleted file mode 100644 index 1689e09..0000000 --- a/src/Command/CreateReactionHandler.php +++ /dev/null @@ -1,73 +0,0 @@ -validator = $validator; - $this->events = $events; - } - - /** - * @param CreateReaction $command - * - * @throws PermissionDeniedException - * - * @return Reaction - */ - public function handle(CreateReaction $command) - { - $actor = $command->actor; - $data = $command->data; - - $actor->assertAdmin(); - - $reaction = Reaction::build( - Arr::get($data, 'identifier'), - Arr::get($data, 'type') - ); - - $this->events->dispatch(new Creating($reaction, $actor, $data)); - - $this->validator->assertValid($reaction->getAttributes()); - - $reaction->save(); - - $this->events->dispatch(new Created($reaction, $actor)); - - return $reaction; - } -} diff --git a/src/Command/DeleteReaction.php b/src/Command/DeleteReaction.php deleted file mode 100644 index 20eaf04..0000000 --- a/src/Command/DeleteReaction.php +++ /dev/null @@ -1,52 +0,0 @@ -reactionId = $reactionId; - $this->actor = $actor; - $this->data = $data; - } -} diff --git a/src/Command/DeleteReactionHandler.php b/src/Command/DeleteReactionHandler.php deleted file mode 100644 index fbf2d95..0000000 --- a/src/Command/DeleteReactionHandler.php +++ /dev/null @@ -1,56 +0,0 @@ -events = $events; - } - - /** - * @param DeleteReaction $command - * - * @throws PermissionDeniedException - * - * @return void - */ - public function handle(DeleteReaction $command) - { - $actor = $command->actor; - - $actor->assertAdmin(); - - $reaction = Reaction::where('id', $command->reactionId)->first(); - - $this->events->dispatch(new Deleting($reaction, $actor)); - - $reaction->delete(); - - $this->events->dispatch(new Deleted($reaction, $actor)); - } -} diff --git a/src/Command/EditReaction.php b/src/Command/EditReaction.php deleted file mode 100644 index 0184adc..0000000 --- a/src/Command/EditReaction.php +++ /dev/null @@ -1,50 +0,0 @@ -reactionId = $reactionId; - $this->actor = $actor; - $this->data = $data; - } -} diff --git a/src/Command/EditReactionHandler.php b/src/Command/EditReactionHandler.php deleted file mode 100644 index ac9498f..0000000 --- a/src/Command/EditReactionHandler.php +++ /dev/null @@ -1,73 +0,0 @@ -validator = $validator; - } - - /** - * @param EditReaction $command - * - * @throws PermissionDeniedException - * - * @return Reaction - */ - public function handle(EditReaction $command) - { - $actor = $command->actor; - $data = $command->data; - - $actor->assertAdmin(); - - $reaction = Reaction::query()->where('id', $command->reactionId)->firstOrFail(); - - if (isset($data['identifier'])) { - $reaction->identifier = $data['identifier']; - } - - if (isset($data['display'])) { - $reaction->display = $data['display']; - } - - if (isset($data['type'])) { - $reaction->type = $data['type']; - } - - if (isset($data['enabled'])) { - $reaction->enabled = $data['enabled']; - } - - $this->validator->assertValid($reaction->getDirty()); - - if ($reaction->isDirty()) { - $reaction->save(); - } - - return $reaction; - } -} diff --git a/src/ForumResourceFields.php b/src/ForumResourceFields.php new file mode 100644 index 0000000..afe8e0d --- /dev/null +++ b/src/ForumResourceFields.php @@ -0,0 +1,38 @@ +get(fn () => [ + $this->settings->get('fof-reactions.convertToUpvote'), + $this->settings->get('fof-reactions.convertToDownvote'), + $this->settings->get('fof-reactions.convertToLike'), + ]), + + Schema\Relationship\ToMany::make('reactions') + ->includable() + ->get(fn () => Reaction::all()), + ]; + } +} diff --git a/src/PostResourceEndpoints.php b/src/PostResourceEndpoints.php new file mode 100644 index 0000000..e76686c --- /dev/null +++ b/src/PostResourceEndpoints.php @@ -0,0 +1,63 @@ +route('DELETE', '/{id}/reactions/specific/{postReactionId}') + ->action($this->action(...)) + ->response(fn () => new EmptyResponse(204)), + Endpoint::make('reactions.type.delete') + ->route('DELETE', '/{id}/reactions/type/{reactionId}') + ->action($this->action(...)) + ->response(fn () => new EmptyResponse(204)), + ]; + } + + protected function action(Context $context): null + { + $actor = $context->getActor(); + + /** @var Post $post */ + $post = $context->model; + + $postReactionId = $context->queryParam('postReactionId'); + $reactionId = $context->queryParam('reactionId'); + + if ($reactionId) { + // Delete all post_reactions of a specific type (i.e. `reaction_id`) + $actor->assertCan('deleteReactions', $post); + + PostReaction::query()->where('post_id', $post->id)->where('reaction_id', $reactionId)->delete(); + PostAnonymousReaction::query()->where('post_id', $post->id)->where('reaction_id', $reactionId)->delete(); + } elseif ($postReactionId) { + // Delete a specific post_reaction for the post + /** + * @var PostReaction $reaction + */ + $reaction = PostReaction::query()->where('post_id', $post->id)->where('id', $postReactionId)->firstOrFail(); + + // If the post is not the actor's, they must have permission to delete reactions + if ($reaction->user_id != $actor->id) { + $actor->assertCan('deleteReactions', $post); + } else { + $actor->assertCan('react', $post); + } + + $reaction->delete(); + } + + // TODO should this send pusher updates? would need new type for non-specific, otherwise could spam pusher events + + return null; + } +} diff --git a/src/PostAttributes.php b/src/PostResourceFields.php similarity index 68% rename from src/PostAttributes.php rename to src/PostResourceFields.php index 5779b8d..ba87ef2 100644 --- a/src/PostAttributes.php +++ b/src/PostResourceFields.php @@ -11,40 +11,32 @@ namespace FoF\Reactions; -use Flarum\Api\Serializer\PostSerializer; +use Flarum\Api\Context; +use Flarum\Api\Schema; use Flarum\Post\Post; use Flarum\Settings\SettingsRepositoryInterface; use Flarum\User\User; use Psr\Http\Message\ServerRequestInterface; -class PostAttributes +class PostResourceFields { - /** @var SettingsRepositoryInterface */ - protected $settings; - - public function __construct(SettingsRepositoryInterface $settings) - { - $this->settings = $settings; + public function __construct( + protected SettingsRepositoryInterface $settings + ) { } - public function __invoke(PostSerializer $serializer, Post $post, array $attributes): array + public function __invoke(): array { - $actor = $serializer->getActor(); - - $attributes['canReact'] = (bool) $actor->can('react', $post); - $attributes['canDeletePostReactions'] = (bool) $actor->can('deleteReactions', $post); - - // Get reaction counts for the post. - $reactionCounts = $this->getReactionCountsForPost($post); - - // Get user's reaction to the post. - $userReaction = $this->getActorReactionForPost($actor, $post, $serializer->getRequest()); - - // Add custom attributes. - $attributes['reactionCounts'] = $reactionCounts; - $attributes['userReactionIdentifier'] = $userReaction; - - return $attributes; + return [ + Schema\Boolean::make('canReact') + ->get(fn (Post $post, Context $context) => $context->getActor()->can('react', $post)), + Schema\Boolean::make('canDeletePostReactions') + ->get(fn (Post $post, Context $context) => $context->getActor()->can('deleteReactions', $post)), + Schema\Number::make('reactionCounts') + ->get(fn (Post $post) => $this->getReactionCountsForPost($post)), + Schema\Number::make('userReactionIdentifier') + ->get(fn (Post $post, Context $context) => $this->getActorReactionForPost($context->getActor(), $post, $context->request)), + ]; } protected function getReactionCountsForPost(Post $post): array diff --git a/src/ReactionsForumAttributes.php b/src/ReactionsForumAttributes.php deleted file mode 100644 index 091092c..0000000 --- a/src/ReactionsForumAttributes.php +++ /dev/null @@ -1,39 +0,0 @@ -settings = $settings; - } - - public function __invoke(ForumSerializer $serializer): array - { - $attributes['ReactionConverts'] = [ - $this->settings->get('fof-reactions.convertToUpvote'), - $this->settings->get('fof-reactions.convertToDownvote'), - $this->settings->get('fof-reactions.convertToLike'), - ]; - - return $attributes; - } -} diff --git a/src/Search/Filter/PostFilter.php b/src/Search/Filter/PostFilter.php new file mode 100644 index 0000000..e1087ce --- /dev/null +++ b/src/Search/Filter/PostFilter.php @@ -0,0 +1,39 @@ + + */ +class PostFilter implements FilterInterface +{ + use ValidateFilterTrait; + + public function __construct( + protected PostRepository $posts + ) { + } + + public function getFilterKey(): string + { + return 'post'; + } + + public function filter(SearchState $state, string|array $value, bool $negate): void + { + $value = $this->asInt($value); + + $post = $this->posts->findOrFail($value, $state->getActor()); + + $state->getActor()->assertCan('canSeeReactions', $post->discussion); + + $state->getQuery()->where('post_id', $negate ? '!=' : '=', $value); + } +} diff --git a/src/Search/PostReactionSearcher.php b/src/Search/PostReactionSearcher.php new file mode 100644 index 0000000..e7f9db8 --- /dev/null +++ b/src/Search/PostReactionSearcher.php @@ -0,0 +1,40 @@ +whereNotNull('reaction_id') + ->whereVisibleTo($actor); + + if ($this->settings->get('fof-reactions.anonymousReactions')) { + $query->unionAll( + PostAnonymousReaction::query() + ->whereNotNull('reaction_id') + ->whereVisibleTo($actor) + ->toBase() + ); + } + + return $query; + } +} diff --git a/src/Validator/ReactionValidator.php b/src/Validator/ReactionValidator.php deleted file mode 100644 index 01492aa..0000000 --- a/src/Validator/ReactionValidator.php +++ /dev/null @@ -1,38 +0,0 @@ - [ - 'required', - 'string', - 'unique:reactions', - ], - 'display' => [ - 'nullable', - 'string', - ], - 'type' => [ - 'required', - 'string', - 'regex:/icon|emoji/i', - ], - 'enabled' => [ - 'nullable', - 'bool', - ], - ]; -} From 4730cd0d62fac14a58542136be392a0420576a2c Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 7 Aug 2024 17:23:54 +0100 Subject: [PATCH 08/11] chore(2.0): PHPUnit 9 to 11 changes Flarum 2.0 uses PHPUnit 11. --- tests/integration/api/CreateReactionTest.php | 21 +++----- tests/integration/api/DeleteReactionTest.php | 9 ++-- tests/integration/api/EditReactionTest.php | 17 ++----- tests/integration/api/ForumTest.php | 5 +- .../integration/api/ListPostReactionsTest.php | 29 +++-------- tests/integration/api/ListReactionsTest.php | 5 +- tests/integration/api/PostAttributesTest.php | 17 ++----- tests/integration/api/ReactTest.php | 49 ++++++------------- 8 files changed, 47 insertions(+), 105 deletions(-) diff --git a/tests/integration/api/CreateReactionTest.php b/tests/integration/api/CreateReactionTest.php index 55da001..33ba41d 100644 --- a/tests/integration/api/CreateReactionTest.php +++ b/tests/integration/api/CreateReactionTest.php @@ -14,6 +14,7 @@ use Flarum\Testing\integration\RetrievesAuthorizedUsers; use Flarum\Testing\integration\TestCase; use FoF\Reactions\Reaction; +use PHPUnit\Framework\Attributes\Test; class CreateReactionTest extends TestCase { @@ -32,9 +33,7 @@ public function setUp(): void ]); } - /** - * @test - */ + #[Test] public function admin_can_create_reaction() { $response = $this->send( @@ -75,9 +74,7 @@ public function admin_can_create_reaction() $this->assertEquals('emoji', $reaction->type); } - /** - * @test - */ + #[Test] public function normal_user_cannot_create_reaction() { $response = $this->send( @@ -99,9 +96,7 @@ public function normal_user_cannot_create_reaction() $this->assertEquals(403, $response->getStatusCode()); } - /** - * @test - */ + #[Test] public function cannot_create_reaction_without_identifier() { $response = $this->send( @@ -127,9 +122,7 @@ public function cannot_create_reaction_without_identifier() $this->assertEquals('/data/attributes/identifier', $json['errors'][0]['source']['pointer']); } - /** - * @test - */ + #[Test] public function cannot_create_reaction_without_type() { $response = $this->send( @@ -155,9 +148,7 @@ public function cannot_create_reaction_without_type() $this->assertEquals('/data/attributes/type', $json['errors'][0]['source']['pointer']); } - /** - * @test - */ + #[Test] public function cannot_create_reaction_with_invalid_type() { $response = $this->send( diff --git a/tests/integration/api/DeleteReactionTest.php b/tests/integration/api/DeleteReactionTest.php index ba2e7fa..439d726 100644 --- a/tests/integration/api/DeleteReactionTest.php +++ b/tests/integration/api/DeleteReactionTest.php @@ -14,6 +14,7 @@ use Flarum\Testing\integration\RetrievesAuthorizedUsers; use Flarum\Testing\integration\TestCase; use FoF\Reactions\Reaction; +use PHPUnit\Framework\Attributes\Test; class DeleteReactionTest extends TestCase { @@ -32,9 +33,7 @@ public function setUp(): void ]); } - /** - * @test - */ + #[Test] public function admin_can_delete_reaction() { $this->app(); @@ -52,9 +51,7 @@ public function admin_can_delete_reaction() $this->assertNull(Reaction::find(5)); } - /** - * @test - */ + #[Test] public function normal_user_cannot_delete_reaction() { $response = $this->send( diff --git a/tests/integration/api/EditReactionTest.php b/tests/integration/api/EditReactionTest.php index fc6d03a..b6773c5 100644 --- a/tests/integration/api/EditReactionTest.php +++ b/tests/integration/api/EditReactionTest.php @@ -14,6 +14,7 @@ use Flarum\Testing\integration\RetrievesAuthorizedUsers; use Flarum\Testing\integration\TestCase; use FoF\Reactions\Reaction; +use PHPUnit\Framework\Attributes\Test; class EditReactionTest extends TestCase { @@ -55,9 +56,7 @@ protected function addNewReaction(): int return $response['data']['id']; } - /** - * @test - */ + #[Test] public function admin_can_edit_reaction() { $this->app(); @@ -96,9 +95,7 @@ public function admin_can_edit_reaction() $this->assertTrue($reaction->enabled); } - /** - * @test - */ + #[Test] public function normal_user_cannot_edit_reaction() { $id = $this->addNewReaction(); @@ -118,9 +115,7 @@ public function normal_user_cannot_edit_reaction() $this->assertEquals(403, $response->getStatusCode()); } - /** - * @test - */ + #[Test] public function cannot_edit_reaction_with_invalid_type() { $id = $this->addNewReaction(); @@ -139,9 +134,7 @@ public function cannot_edit_reaction_with_invalid_type() $this->assertEquals(422, $response->getStatusCode()); } - /** - * @test - */ + #[Test] public function cannot_edit_non_existent_reaction() { $response = $this->send( diff --git a/tests/integration/api/ForumTest.php b/tests/integration/api/ForumTest.php index c3a6ceb..02581e7 100644 --- a/tests/integration/api/ForumTest.php +++ b/tests/integration/api/ForumTest.php @@ -12,6 +12,7 @@ namespace FoF\Reactions\tests\integration\api; use Flarum\Testing\integration\TestCase; +use PHPUnit\Framework\Attributes\Test; class ForumTest extends TestCase { @@ -22,9 +23,7 @@ public function setUp(): void $this->extension('fof-reactions'); } - /** - * @test - */ + #[Test] public function forum_relations_are_included() { $response = $this->send( diff --git a/tests/integration/api/ListPostReactionsTest.php b/tests/integration/api/ListPostReactionsTest.php index 6589ca5..0384287 100644 --- a/tests/integration/api/ListPostReactionsTest.php +++ b/tests/integration/api/ListPostReactionsTest.php @@ -15,6 +15,7 @@ use Flarum\Group\Group; use Flarum\Testing\integration\RetrievesAuthorizedUsers; use Flarum\Testing\integration\TestCase; +use PHPUnit\Framework\Attributes\Test; class ListPostReactionsTest extends TestCase { @@ -82,9 +83,7 @@ protected function addGuestViewPermission() ]); } - /** - * @test - */ + #[Test] public function guest_cannot_see_reactions_when_permission_not_given_on_a_post_when_guest_reacting_is_off() { $response = $this->send( @@ -95,9 +94,7 @@ public function guest_cannot_see_reactions_when_permission_not_given_on_a_post_w $this->assertEquals(403, $response->getStatusCode()); } - /** - * @test - */ + #[Test] public function guest_can_see_reactions_when_permission_given_on_a_post_when_guest_reacting_is_off() { $this->addGuestViewPermission(); @@ -139,9 +136,7 @@ public function guest_can_see_reactions_when_permission_given_on_a_post_when_gue $this->assertEquals('users', $response['data'][3]['relationships']['user']['data']['type']); } - /** - * @test - */ + #[Test] public function guest_can_see_reactions_on_a_post_when_guest_reacting_is_on() { $this->setting('fof-reactions.anonymousReactions', true); @@ -159,9 +154,7 @@ public function guest_can_see_reactions_on_a_post_when_guest_reacting_is_on() $this->assertEquals(8, count($response['data'])); } - /** - * @test - */ + #[Test] public function user_with_view_permission_can_see_reactions_on_a_post_when_guest_reacting_is_off() { $response = $this->send( @@ -202,9 +195,7 @@ public function user_with_view_permission_can_see_reactions_on_a_post_when_guest $this->assertEquals('users', $response['data'][3]['relationships']['user']['data']['type']); } - /** - * @test - */ + #[Test] public function user_with_view_permission_can_see_reactions_on_a_post_when_guest_reacting_is_on() { $this->setting('fof-reactions.anonymousReactions', true); @@ -222,9 +213,7 @@ public function user_with_view_permission_can_see_reactions_on_a_post_when_guest $this->assertEquals(8, count($response['data'])); } - /** - * @test - */ + #[Test] public function user_without_view_permission_cannot_see_reactions_on_a_post_when_guest_reacting_is_off() { $response = $this->send( @@ -236,9 +225,7 @@ public function user_without_view_permission_cannot_see_reactions_on_a_post_when $this->assertEquals(403, $response->getStatusCode()); } - /** - * @test - */ + #[Test] public function user_without_view_permission_cannot_see_reactions_on_a_post_when_guest_reacting_is_on() { $this->setting('fof-reactions.anonymousReactions', true); diff --git a/tests/integration/api/ListReactionsTest.php b/tests/integration/api/ListReactionsTest.php index 2fd9f15..085cca0 100644 --- a/tests/integration/api/ListReactionsTest.php +++ b/tests/integration/api/ListReactionsTest.php @@ -13,6 +13,7 @@ use Flarum\Testing\integration\TestCase; use FoF\Reactions\Reaction; +use PHPUnit\Framework\Attributes\Test; class ListReactionsTest extends TestCase { @@ -23,9 +24,7 @@ public function setUp(): void $this->extension('fof-reactions'); } - /** - * @test - */ + #[Test] public function it_returns_all_reactions() { $response = $this->send( diff --git a/tests/integration/api/PostAttributesTest.php b/tests/integration/api/PostAttributesTest.php index 8bbbdb5..714f0ad 100644 --- a/tests/integration/api/PostAttributesTest.php +++ b/tests/integration/api/PostAttributesTest.php @@ -14,6 +14,7 @@ use Carbon\Carbon; use Flarum\Testing\integration\RetrievesAuthorizedUsers; use Flarum\Testing\integration\TestCase; +use PHPUnit\Framework\Attributes\Test; class PostAttributesTest extends TestCase { @@ -72,9 +73,7 @@ protected function arrayOfReactionCounts(int $anon = 0): array ]; } - /** - * @test - */ + #[Test] public function user_has_correct_post_attributes_when_anonymous_reactions_disabled() { $response = $this->send( @@ -92,9 +91,7 @@ public function user_has_correct_post_attributes_when_anonymous_reactions_disabl $this->assertEquals(1, $body['data']['attributes']['userReactionIdentifier'], 'User has reacted with reaction id 1'); } - /** - * @test - */ + #[Test] public function guest_has_correct_post_attributes_when_anonymous_reactions_disabled() { $response = $this->send( @@ -111,9 +108,7 @@ public function guest_has_correct_post_attributes_when_anonymous_reactions_disab $this->assertEquals(null, $body['data']['attributes']['userReactionIdentifier'], 'User has reacted with reaction id 1'); } - /** - * @test - */ + #[Test] public function user_has_correct_post_attributes_when_anonymous_reactions_enabled() { $this->setting('fof-reactions.anonymousReactions', true); @@ -133,9 +128,7 @@ public function user_has_correct_post_attributes_when_anonymous_reactions_enable $this->assertEquals(1, $body['data']['attributes']['userReactionIdentifier'], 'User has reacted with reaction id 1'); } - /** - * @test - */ + #[Test] public function guest_has_correct_post_attributes_when_anonymous_reactions_enabled() { $this->setting('fof-reactions.anonymousReactions', true); diff --git a/tests/integration/api/ReactTest.php b/tests/integration/api/ReactTest.php index 91f6888..8cd21b6 100644 --- a/tests/integration/api/ReactTest.php +++ b/tests/integration/api/ReactTest.php @@ -19,6 +19,8 @@ use FoF\Reactions\PostAnonymousReaction; use FoF\Reactions\PostReaction; use Psr\Http\Message\ResponseInterface; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; class ReactTest extends TestCase { @@ -72,11 +74,8 @@ protected function rewriteDefaultPermissionsAfterBoot() $this->database()->table('group_permission')->insert(['permission' => 'discussion.reactPosts', 'group_id' => 5]); } - /** - * @dataProvider allowedUsersToReact - * - * @test - */ + #[Test] + #[DataProvider('allowedUsersToReact')] public function can_react_to_a_post_if_allowed(int $postId, ?int $authenticatedAs, int $reactionId, string $message, bool $canReactOwnPost = null, bool $guestReactionsEnabled = null) { if (!is_null($canReactOwnPost)) { @@ -117,11 +116,8 @@ public function can_react_to_a_post_if_allowed(int $postId, ?int $authenticatedA } } - /** - * @dataProvider unallowedUsersToReact - * - * @test - */ + #[Test] + #[DataProvider('unallowedUsersToReact')] public function cannot_react_to_a_post_if_not_allowed(int $postId, ?int $authenticatedAs, int $reactionId, string $message, bool $canReactOwnPost = null, bool $guestReactionsEnabled = null) { if (!is_null($canReactOwnPost)) { @@ -147,7 +143,7 @@ public function cannot_react_to_a_post_if_not_allowed(int $postId, ?int $authent } } - public function allowedUsersToReact(): array + public static function allowedUsersToReact(): array { return [ // [$postId, $authAs, $reactionId, $message, $canReactOwnPost, $guestReactionsEnabled] @@ -160,7 +156,7 @@ public function allowedUsersToReact(): array ]; } - public function unallowedUsersToReact(): array + public static function unallowedUsersToReact(): array { return [ // [$postId, $authAs, $reactionId, $message, $canReactOwnPost, $guestReactionsEnabled] @@ -211,9 +207,7 @@ protected function disableReactionId(int $reactionId): void $this->database()->table('reactions')->where('id', $reactionId)->update(['enabled' => false]); } - /** - * @test - */ + #[Test] public function user_cannot_react_to_a_post_if_reaction_disabled() { $this->disableReactionId(1); @@ -231,9 +225,7 @@ public function user_cannot_react_to_a_post_if_reaction_disabled() $this->assertNull($postReaction, 'Reaction was saved to database'); } - /** - * @test - */ + #[Test] public function guest_cannot_react_to_a_post_when_feature_is_enabled_but_reaction_disabled() { $this->setting('fof-reactions.anonymousReactions', true); @@ -252,11 +244,8 @@ public function guest_cannot_react_to_a_post_when_feature_is_enabled_but_reactio $this->assertNull($postReaction, 'Anonymous reaction was saved to database'); } - /** - * @dataProvider deleteSpecificPostReactionUsersData - * - * @test - */ + #[Test] + #[DataProvider('deleteSpecificPostReactionUsersData')] public function user_can_delete_own_post_reaction_by_id($reactionAs, $authAs, $message, $statusCode) { $this->sendReactRequest(1, 1, $reactionAs); @@ -299,7 +288,7 @@ public function user_can_delete_own_post_reaction_by_id($reactionAs, $authAs, $m } } - public function deleteSpecificPostReactionUsersData() + public static function deleteSpecificPostReactionUsersData() { return [ // [$reactionAs, $authAs, $message, $statusCode] @@ -311,9 +300,7 @@ public function deleteSpecificPostReactionUsersData() ]; } - /** - * @test - */ + #[Test] public function user_with_permission_can_react_and_is_converted_to_like_when_likes_is_enabled() { $this->extension('flarum-likes'); @@ -334,9 +321,7 @@ public function user_with_permission_can_react_and_is_converted_to_like_when_lik $this->assertTrue($likes->contains(3), 'User is in the collection of users who liked the post'); } - /** - * @test - */ + #[Test] public function user_with_permission_can_react_and_not_have_it_converted_to_a_like() { $this->extension('flarum-likes'); @@ -355,9 +340,7 @@ public function user_with_permission_can_react_and_not_have_it_converted_to_a_li $this->assertCount(0, $likes); } - /** - * @test - */ + #[Test] public function empty_string_as_convert_like_setting_does_nothing() { $this->extension('flarum-likes'); From 6464bdf05db46e21f28492207f81987087574937 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 7 Aug 2024 17:24:11 +0100 Subject: [PATCH 09/11] chore(2.0): `LESS` code changes Many variables have been renamed to light/dark specific names and most are now used as CSS variables instead. --- resources/less/forum.less | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/less/forum.less b/resources/less/forum.less index d1df1fb..a47670a 100644 --- a/resources/less/forum.less +++ b/resources/less/forum.less @@ -50,8 +50,8 @@ } .CommentPost--Reactions { - background-color: @body-bg; - color: @text-color; + background-color: var(--body-bg); + color: var(--text-color); visibility: hidden; opacity: 0; transition: visibility 0s linear 300ms, opacity 300ms; @@ -111,7 +111,7 @@ margin: 0; a { - color: @text-color; + color: var(--text-color); font-size: 15px; display: block; margin-bottom: 10px; From 3b600c0dd33a6960744e83812a5aa11f335f1b5c Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Fri, 25 Oct 2024 11:45:03 +0100 Subject: [PATCH 10/11] fix: misc --- extend.php | 5 +- js/package.json | 4 +- js/src/admin/extend.js | 38 ++++ js/src/admin/index.js | 40 +--- js/src/common/util/emoji.js | 4 +- js/src/forum/components/ReactionsModal.tsx | 5 +- js/src/forum/utils/groupBy.ts | 13 +- src/ForumResourceFields.php | 2 +- src/Listener/SaveReactionsToDatabase.php | 237 --------------------- src/Notification/PostReactedBlueprint.php | 3 +- src/PostReaction.php | 3 + src/PostResourceFields.php | 172 ++++++++++++++- 12 files changed, 231 insertions(+), 295 deletions(-) create mode 100644 js/src/admin/extend.js delete mode 100755 src/Listener/SaveReactionsToDatabase.php diff --git a/extend.php b/extend.php index 72af3ba..b385623 100644 --- a/extend.php +++ b/extend.php @@ -14,7 +14,6 @@ use Flarum\Api\Context; use Flarum\Discussion\Discussion; use Flarum\Extend; -use Flarum\Post\Event\Saving; use Flarum\Post\Post; use Flarum\Search\Database\DatabaseSearchDriver; use FoF\Reactions\Notification\PostReactedBlueprint; @@ -41,7 +40,6 @@ ->scope(Access\ScopePostReactionVisibility::class), (new Extend\Event()) - ->listen(Saving::class, Listener\SaveReactionsToDatabase::class) ->subscribe(Listener\SendNotifications::class), (new Extend\Notification()) @@ -56,8 +54,7 @@ ->endpoint(Endpoint\Show::class, fn (Endpoint\Show $show) => $show->addDefaultInclude(['reactions'])), (new Extend\ApiResource(Resource\PostResource::class)) - ->fields(PostResourceFields::class) - ->endpoints(PostRso), + ->fields(PostResourceFields::class), (new Extend\ApiResource(Resource\DiscussionResource::class)) ->fields(fn (): array => [ diff --git a/js/package.json b/js/package.json index 27bb14b..289fc08 100644 --- a/js/package.json +++ b/js/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@flarum/prettier-config": "^1.0.0", - "flarum-tsconfig": "^1.0.2", + "flarum-tsconfig": "^2.0.0", "flarum-webpack-config": "^3.0.0", "fuzzyset": "^1.0.7", "lodash.debounce": "^4.0.8", @@ -22,4 +22,4 @@ "devDependencies": { "prettier": "^2.8.8" } -} \ No newline at end of file +} diff --git a/js/src/admin/extend.js b/js/src/admin/extend.js new file mode 100644 index 0000000..66ecb6d --- /dev/null +++ b/js/src/admin/extend.js @@ -0,0 +1,38 @@ +import Extend from 'flarum/common/extenders'; +import SettingsPage from './components/SettingsPage'; +import Reaction from '../common/models/Reaction'; +import Forum from 'flarum/common/models/Forum'; + +export default [ + new Extend.Store().add('reactions', Reaction), + + new Extend.Model(Forum).hasMany('reactions', Reaction), + + new Extend.Admin() + .permission( + () => ({ + icon: 'far fa-thumbs-up', + label: app.translator.trans('fof-reactions.admin.permissions.react_posts_label'), + permission: 'discussion.reactPosts', + }), + 'reply' + ) + .permission( + () => ({ + icon: 'fas fa-info-circle', + label: app.translator.trans('fof-reactions.admin.permissions.see_reactions_label'), + permission: 'discussion.canSeeReactions', + allowGuest: true, + }), + 'view' + ) + .permission( + () => ({ + icon: 'fas fa-trash', + label: app.translator.trans('fof-reactions.admin.permissions.delete_post_reactions_label'), + permission: 'discussion.deleteReactionsPosts', + }), + 'moderate' + ) + .page(SettingsPage), +]; diff --git a/js/src/admin/index.js b/js/src/admin/index.js index b4b6c43..873be3f 100644 --- a/js/src/admin/index.js +++ b/js/src/admin/index.js @@ -1,46 +1,12 @@ import app from 'flarum/admin/app'; -import Forum from 'flarum/common/models/Forum'; -import Model from 'flarum/common/Model'; - -import SettingsPage from './components/SettingsPage'; -import Reaction from '../common/models/Reaction'; export * from './components'; export * from '../common/components'; export * from '../common/models'; export * from '../common/util'; -app.initializers.add('fof/reactions', () => { - app.store.models.reactions = Reaction; - - Forum.prototype.reactions = Model.hasMany('reactions'); +export { default as extend } from './extend'; - app.extensionData - .for('fof-reactions') - .registerPermission( - { - icon: 'far fa-thumbs-up', - label: app.translator.trans('fof-reactions.admin.permissions.react_posts_label'), - permission: 'discussion.reactPosts', - }, - 'reply' - ) - .registerPermission( - { - icon: 'fas fa-info-circle', - label: app.translator.trans('fof-reactions.admin.permissions.see_reactions_label'), - permission: 'discussion.canSeeReactions', - allowGuest: true, - }, - 'view' - ) - .registerPermission( - { - icon: 'fas fa-trash', - label: app.translator.trans('fof-reactions.admin.permissions.delete_post_reactions_label'), - permission: 'discussion.deleteReactionsPosts', - }, - 'moderate' - ) - .registerPage(SettingsPage); +app.initializers.add('fof/reactions', () => { + // }); diff --git a/js/src/common/util/emoji.js b/js/src/common/util/emoji.js index c133891..54706f0 100755 --- a/js/src/common/util/emoji.js +++ b/js/src/common/util/emoji.js @@ -24,7 +24,7 @@ const search = (query) => { }; }; -export default (reactionOrIdentifier) => { +export default function emoji(reactionOrIdentifier) { if (!reactionOrIdentifier) return {}; let identifier = reactionOrIdentifier.identifier || reactionOrIdentifier; @@ -56,4 +56,4 @@ export default (reactionOrIdentifier) => { emojiCache.set(reactionOrIdentifier, output); return output || {}; -}; +} diff --git a/js/src/forum/components/ReactionsModal.tsx b/js/src/forum/components/ReactionsModal.tsx index 56d9369..8a05b16 100644 --- a/js/src/forum/components/ReactionsModal.tsx +++ b/js/src/forum/components/ReactionsModal.tsx @@ -110,7 +110,10 @@ export default class ReactionsModal extends Modal { async load(): Promise { this.loading = true; - const response = await app.store.find>(`/posts_reactions`, { include: 'user,reaction', filter: { post: this.attrs.post.id() } }); + const response = await app.store.find>(`/posts_reactions`, { + include: 'user,reaction', + filter: { post: this.attrs.post.id() }, + }); const groupedReactions = groupBy(response, (r: PostReaction) => r.reactionId()); const reactions: ReactionGroup[] = []; diff --git a/js/src/forum/utils/groupBy.ts b/js/src/forum/utils/groupBy.ts index f302d77..62c8c41 100644 --- a/js/src/forum/utils/groupBy.ts +++ b/js/src/forum/utils/groupBy.ts @@ -1,12 +1,9 @@ export type GroupByFunction = (val: T) => string | number; -const groupBy = (arr: T[], fn: GroupByFunction | keyof T): Record => - arr.map(typeof fn === 'function' ? fn : (val) => (val as any)[fn]).reduce( - (acc, val, i) => { - acc[val] = (acc[val] || []).concat(arr[i]); - return acc; - }, - {} as Record - ); +const groupBy = (arr: T[], fn: GroupByFunction | keyof T): Record => + arr.map(typeof fn === 'function' ? fn : (val) => (val as any)[fn]).reduce((acc, val, i) => { + acc[val] = (acc[val] || []).concat(arr[i]); + return acc; + }, {} as Record); export default groupBy; diff --git a/src/ForumResourceFields.php b/src/ForumResourceFields.php index afe8e0d..9d04bdf 100644 --- a/src/ForumResourceFields.php +++ b/src/ForumResourceFields.php @@ -32,7 +32,7 @@ public function __invoke(): array Schema\Relationship\ToMany::make('reactions') ->includable() - ->get(fn () => Reaction::all()), + ->get(fn () => Reaction::all()->all()), ]; } } diff --git a/src/Listener/SaveReactionsToDatabase.php b/src/Listener/SaveReactionsToDatabase.php deleted file mode 100755 index 040b7dc..0000000 --- a/src/Listener/SaveReactionsToDatabase.php +++ /dev/null @@ -1,237 +0,0 @@ -settings = $settings; - $this->translator = $translator; - $this->extensions = $extensions; - $this->events = $events; - } - - /** - * @param Saving $event - * - * @throws \Flarum\User\Exception\PermissionDeniedException - * @throws \Flarum\Foundation\ValidationException - */ - public function handle(Saving $event) - { - $post = $event->post; - $data = $event->data; - - if ($post->exists && Arr::has($data, 'attributes.reaction')) { - $actor = $event->actor; - - $reactionId = Arr::get($data, 'attributes.reaction'); - - $actor->assertCan('react', $post); - - $reaction = !is_null($reactionId) ? Reaction::where('id', $reactionId)->first() : null; - - $this->events->dispatch(new WillReactToPost($post, $actor, $reaction)); - - $gamification = $this->isExtensionEnabled('fof-gamification'); - $likes = $this->isExtensionEnabled('flarum-likes'); - - $gamificationUpvote = $this->settings->get('fof-reactions.convertToUpvote'); - $gamificationDownvote = $this->settings->get('fof-reactions.convertToDownvote'); - - if ($gamification && class_exists(SaveVotesToDatabase::class) && $reaction && $reaction->identifier == $gamificationUpvote) { - resolve(SaveVotesToDatabase::class)->vote( - $post, - false, - true, - $actor, - $post->user - ); - } elseif ($gamification && class_exists(SaveVotesToDatabase::class) && $reaction && $reaction->identifier == $gamificationDownvote) { - resolve(SaveVotesToDatabase::class)->vote( - $post, - true, - false, - $actor, - $post->user - ); - } elseif ($likes && $reaction && $reaction->identifier == $this->settings->get('fof-reactions.convertToLike')) { - /** @phpstan-ignore-next-line */ - $liked = $post->likes()->where('user_id', $actor->id)->exists(); - - if ($liked) { - return; - } else { - // TODO: we should probably start checking permission to like here - //$actor->assertCan('like', $post); - - /** @phpstan-ignore-next-line */ - $post->likes()->attach($actor->id); - - $post->raise(new PostWasLiked($post, $actor)); - } - } else { - $guestId = $this->getSessionId(); - if ($actor->isGuest()) { - $postReaction = PostAnonymousReaction::where([['guest_id', $guestId], ['post_id', $post->id]])->first(); - } else { - $postReaction = PostReaction::where([['user_id', $actor->id], ['post_id', $post->id]])->first(); - } - $removeReaction = is_null($reactionId) || ($postReaction && $postReaction->reaction_id == $reactionId); - - if ($removeReaction) { - if ($postReaction) { - // We'll maintain current behaviour and only push to Pusher if the reaction is not anonymous - if ($postReaction instanceof PostReaction) { - $this->push('removedReaction', $postReaction, $reaction ?: $postReaction->reaction, $actor, $post); - } - - $postReaction->reaction_id = null; - $postReaction->save(); - } - - $post->raise(new PostWasUnreacted($post, $postReaction, $actor)); - } else { - $this->validateReaction($reactionId); - - if ($postReaction) { - $postReaction->reaction_id = $reaction->id; - $postReaction->save(); - } else { - $isGuest = $actor->isGuest(); - $postReaction = $isGuest ? new PostAnonymousReaction() : new PostReaction(); - - $postReaction->post_id = $post->id; - - if ($isGuest) { - $postReaction->guest_id = $guestId; - } else { - $postReaction->user_id = $actor->id; - } - - $postReaction->reaction_id = $reaction->id; - - $postReaction->save(); - } - - // We'll maintain current behaviour and only push to Pusher if the reaction is not anonymous - if ($postReaction instanceof PostReaction) { - $this->push('newReaction', $postReaction, $reaction, $actor, $post); - } - - $post->raise(new PostWasReacted($post, $postReaction, $actor, $reaction)); - } - } - } - } - - /** - * @param $event - * @param PostReaction $postReaction - * @param Reaction $reaction - * @param User $actor - * @param Post $post - */ - public function push($event, PostReaction $postReaction, Reaction $reaction, User $actor, Post $post) - { - if ($pusher = $this->getPusher()) { - $pusher->trigger('public', $event, [ - 'id' => (string) $postReaction->id, - 'reactionId' => $reaction->id, - 'postId' => $post->id, - 'userId' => $actor->id, - ]); - } - } - - /** - * @return bool|\Illuminate\Foundation\Application|mixed|Pusher - */ - private function getPusher() - { - if (class_exists(Pusher::class) && resolve('container')->bound(Pusher::class)) { - return resolve(Pusher::class); - } - - return false; - } - - protected function validateReaction($reactionId) - { - if (is_null($reactionId)) { - return; - } - - $reaction = Reaction::find($reactionId); - - if (!$reaction || !$reaction->enabled) { - throw new ValidationException([ - 'reaction' => $this->translator->trans('fof-reactions.forum.disabled-reaction'), - ]); - } - } - - protected function isExtensionEnabled(string $extension): bool - { - return $this->extensions->isEnabled($extension); - } - - protected function getSessionId(): ?string - { - /** @var ServerRequestInterface $request */ - $request = resolve('fof-reactions.request'); - $session = $request->getAttribute('session'); - - return $session ? $session->getId() : null; - } -} diff --git a/src/Notification/PostReactedBlueprint.php b/src/Notification/PostReactedBlueprint.php index b41f8e5..f647fe1 100755 --- a/src/Notification/PostReactedBlueprint.php +++ b/src/Notification/PostReactedBlueprint.php @@ -12,11 +12,12 @@ namespace FoF\Reactions\Notification; use Flarum\Database\AbstractModel; +use Flarum\Notification\AlertableInterface; use Flarum\Notification\Blueprint\BlueprintInterface; use Flarum\Post\Post; use Flarum\User\User; -class PostReactedBlueprint implements BlueprintInterface +class PostReactedBlueprint implements BlueprintInterface, AlertableInterface { /** * @var Post diff --git a/src/PostReaction.php b/src/PostReaction.php index bb5026e..db70716 100644 --- a/src/PostReaction.php +++ b/src/PostReaction.php @@ -12,6 +12,7 @@ namespace FoF\Reactions; use Flarum\Database\AbstractModel; +use Flarum\Database\ScopeVisibilityTrait; use Flarum\Post\Post; use Flarum\User\User; @@ -29,6 +30,8 @@ */ class PostReaction extends AbstractModel { + use ScopeVisibilityTrait; + protected $table = 'post_reactions'; public $timestamps = true; diff --git a/src/PostResourceFields.php b/src/PostResourceFields.php index ba87ef2..a3efe07 100644 --- a/src/PostResourceFields.php +++ b/src/PostResourceFields.php @@ -13,15 +13,27 @@ use Flarum\Api\Context; use Flarum\Api\Schema; +use Flarum\Extension\ExtensionManager; +use Flarum\Foundation\ValidationException; +use Flarum\Likes\Event\PostWasLiked; +use Flarum\Locale\TranslatorInterface; use Flarum\Post\Post; use Flarum\Settings\SettingsRepositoryInterface; use Flarum\User\User; +use FoF\Gamification\Listeners\SaveVotesToDatabase; +use FoF\Reactions\Event\PostWasReacted; +use FoF\Reactions\Event\PostWasUnreacted; +use FoF\Reactions\Event\WillReactToPost; +use Illuminate\Contracts\Events\Dispatcher; use Psr\Http\Message\ServerRequestInterface; class PostResourceFields { public function __construct( - protected SettingsRepositoryInterface $settings + protected SettingsRepositoryInterface $settings, + protected Dispatcher $events, + protected ExtensionManager $extensions, + protected TranslatorInterface $translator ) { } @@ -32,10 +44,15 @@ public function __invoke(): array ->get(fn (Post $post, Context $context) => $context->getActor()->can('react', $post)), Schema\Boolean::make('canDeletePostReactions') ->get(fn (Post $post, Context $context) => $context->getActor()->can('deleteReactions', $post)), - Schema\Number::make('reactionCounts') + Schema\Arr::make('reactionCounts') ->get(fn (Post $post) => $this->getReactionCountsForPost($post)), Schema\Number::make('userReactionIdentifier') ->get(fn (Post $post, Context $context) => $this->getActorReactionForPost($context->getActor(), $post, $context->request)), + + Schema\Str::make('reaction') + ->hidden() + ->writableOnUpdate() + ->save($this->setReaction(...)), ]; } @@ -86,4 +103,155 @@ protected function getActorReactionForPost(User $actor, Post $post, ServerReques ->where('user_id', $actor->id) ->value('reaction_id'); } + + protected function setReaction(Post $post, ?string $reactionId, Context $context): void + { + if ($post->exists) { + $actor = $context->getActor(); + + $actor->assertCan('react', $post); + + $reaction = !is_null($reactionId) ? Reaction::where('id', $reactionId)->first() : null; + + $this->events->dispatch(new WillReactToPost($post, $actor, $reaction)); + + $gamification = $this->extensions->isEnabled('fof-gamification'); + $likes = $this->extensions->isEnabled('flarum-likes'); + + $gamificationUpvote = $this->settings->get('fof-reactions.convertToUpvote'); + $gamificationDownvote = $this->settings->get('fof-reactions.convertToDownvote'); + + if ($gamification && class_exists(SaveVotesToDatabase::class) && $reaction && $reaction->identifier == $gamificationUpvote) { + resolve(SaveVotesToDatabase::class)->vote( + $post, + false, + true, + $actor, + $post->user + ); + } elseif ($gamification && class_exists(SaveVotesToDatabase::class) && $reaction && $reaction->identifier == $gamificationDownvote) { + resolve(SaveVotesToDatabase::class)->vote( + $post, + true, + false, + $actor, + $post->user + ); + } elseif ($likes && $reaction && $reaction->identifier == $this->settings->get('fof-reactions.convertToLike')) { + /** @phpstan-ignore-next-line */ + $liked = $post->likes()->where('user_id', $actor->id)->exists(); + + if ($liked) { + return; + } else { + // TODO: we should probably start checking permission to like here + //$actor->assertCan('like', $post); + + /** @phpstan-ignore-next-line */ + $post->likes()->attach($actor->id); + + $post->raise(new PostWasLiked($post, $actor)); + } + } else { + $guestId = $context->request->getAttribute('session')?->getId(); + + if ($actor->isGuest()) { + $postReaction = PostAnonymousReaction::where([['guest_id', $guestId], ['post_id', $post->id]])->first(); + } else { + $postReaction = PostReaction::where([['user_id', $actor->id], ['post_id', $post->id]])->first(); + } + + $removeReaction = is_null($reactionId) || ($postReaction && $postReaction->reaction_id == $reactionId); + + if ($removeReaction) { + if ($postReaction) { + // We'll maintain current behaviour and only push to Pusher if the reaction is not anonymous + if ($postReaction instanceof PostReaction) { + $this->push('removedReaction', $postReaction, $reaction ?: $postReaction->reaction, $actor, $post); + } + + $postReaction->reaction_id = null; + $postReaction->save(); + } + + $post->raise(new PostWasUnreacted($post, $postReaction, $actor)); + } else { + $this->validateReaction($reactionId); + + if ($postReaction) { + $postReaction->reaction_id = $reaction->id; + $postReaction->save(); + } else { + $isGuest = $actor->isGuest(); + $postReaction = $isGuest ? new PostAnonymousReaction() : new PostReaction(); + + $postReaction->post_id = $post->id; + + if ($isGuest) { + $postReaction->guest_id = $guestId; + } else { + $postReaction->user_id = $actor->id; + } + + $postReaction->reaction_id = $reaction->id; + + $postReaction->save(); + } + + // We'll maintain current behaviour and only push to Pusher if the reaction is not anonymous + if ($postReaction instanceof PostReaction) { + $this->push('newReaction', $postReaction, $reaction, $actor, $post); + } + + $post->raise(new PostWasReacted($post, $postReaction, $actor, $reaction)); + } + } + } + } + + /** + * @param $event + * @param PostReaction $postReaction + * @param Reaction $reaction + * @param User $actor + * @param Post $post + */ + public function push($event, PostReaction $postReaction, Reaction $reaction, User $actor, Post $post) + { + if ($pusher = $this->getPusher()) { + $pusher->trigger('public', $event, [ + 'id' => (string) $postReaction->id, + 'reactionId' => $reaction->id, + 'postId' => $post->id, + 'userId' => $actor->id, + ]); + } + } + + /** + * @return bool|\Illuminate\Foundation\Application|mixed|Pusher + */ + private function getPusher() + { + if (class_exists(Pusher::class) && resolve('container')->bound(Pusher::class)) { + return resolve(Pusher::class); + } + + return false; + } + + protected function validateReaction($reactionId) + { + if (is_null($reactionId)) { + return; + } + + $reaction = Reaction::find($reactionId); + + if (!$reaction || !$reaction->enabled) { + throw new ValidationException([ + 'reaction' => $this->translator->trans('fof-reactions.forum.disabled-reaction'), + ]); + } + } } From 84b3127166481c8548d11f5cba7bc2d5464f3d24 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Fri, 25 Oct 2024 12:14:43 +0100 Subject: [PATCH 11/11] fix: misc --- js/src/forum/components/ReactionsModal.tsx | 9 ++- src/Access/ScopePostReactionVisibility.php | 2 +- .../DeletePostReactionController.php | 68 ------------------- src/Api/Resource/PostReactionResource.php | 47 ++++++++++++- src/Api/Resource/ReactionResource.php | 5 -- 5 files changed, 53 insertions(+), 78 deletions(-) delete mode 100644 src/Api/Controller/DeletePostReactionController.php diff --git a/js/src/forum/components/ReactionsModal.tsx b/js/src/forum/components/ReactionsModal.tsx index 8a05b16..2dd2258 100644 --- a/js/src/forum/components/ReactionsModal.tsx +++ b/js/src/forum/components/ReactionsModal.tsx @@ -110,7 +110,7 @@ export default class ReactionsModal extends Modal { async load(): Promise { this.loading = true; - const response = await app.store.find>(`/posts_reactions`, { + const response = await app.store.find>(`post_reactions`, { include: 'user,reaction', filter: { post: this.attrs.post.id() }, }); @@ -160,7 +160,12 @@ export default class ReactionsModal extends Modal { await app.request({ method: 'DELETE', - url: `${app.forum.attribute('apiUrl')}/posts/${this.attrs.post.id()}/reactions/${isSpecific ? 'specific' : 'type'}/${id}`, + url: `${app.forum.attribute('apiUrl')}/post_reactions/delete`, + body: { + postId: this.attrs.post.id(), + specific: isSpecific, + reactionOrPostReactionId: id, + }, }); // Filter out the deleted reaction type diff --git a/src/Access/ScopePostReactionVisibility.php b/src/Access/ScopePostReactionVisibility.php index 60535ab..299b94c 100644 --- a/src/Access/ScopePostReactionVisibility.php +++ b/src/Access/ScopePostReactionVisibility.php @@ -7,7 +7,7 @@ class ScopePostReactionVisibility { - public function __invoke(Builder $query, User $actor): void + public function __invoke(User $actor, Builder $query): void { $query->whereHas('post', function (Builder $query) use ($actor) { $query->whereVisibleTo($actor); diff --git a/src/Api/Controller/DeletePostReactionController.php b/src/Api/Controller/DeletePostReactionController.php deleted file mode 100644 index 5575d8b..0000000 --- a/src/Api/Controller/DeletePostReactionController.php +++ /dev/null @@ -1,68 +0,0 @@ -getQueryParams(); - - $postId = Arr::get($params, 'id'); - $postReactionId = Arr::get($params, 'postReactionId'); - $reactionId = Arr::get($params, 'reactionId'); - - $post = Post::whereVisibleTo($actor)->findOrFail($postId); - - if ($reactionId) { - // Delete all post_reactions of a specific type (i.e. `reaction_id`) - $actor->assertCan('deleteReactions', $post); - - PostReaction::query()->where('post_id', $postId)->where('reaction_id', $reactionId)->delete(); - PostAnonymousReaction::query()->where('post_id', $postId)->where('reaction_id', $reactionId)->delete(); - } elseif ($postReactionId) { - // Delete a specific post_reaction for the post - /** - * @var PostReaction $reaction - */ - $reaction = PostReaction::query()->where('post_id', $postId)->where('id', $postReactionId)->firstOrFail(); - - // If the post is not the actor's, they must have permission to delete reactions - if ($reaction->user_id != $actor->id) { - $actor->assertCan('deleteReactions', $post); - } else { - $actor->assertCan('react', $post); - } - - $reaction->delete(); - } - - // TODO should this send pusher updates? would need new type for non-specific, otherwise could spam pusher events - - return new EmptyResponse(204); - } -} diff --git a/src/Api/Resource/PostReactionResource.php b/src/Api/Resource/PostReactionResource.php index 668b15e..dca17e6 100644 --- a/src/Api/Resource/PostReactionResource.php +++ b/src/Api/Resource/PostReactionResource.php @@ -7,8 +7,13 @@ use Flarum\Api\Resource; use Flarum\Api\Schema; use Flarum\Api\Sort\SortColumn; +use Flarum\Http\RequestUtil; +use Flarum\Post\Post; +use FoF\Reactions\PostAnonymousReaction; use FoF\Reactions\PostReaction; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Arr; +use Laminas\Diactoros\Response\EmptyResponse; use Tobyz\JsonApiServer\Context as OriginalContext; /** @@ -29,8 +34,46 @@ public function model(): string public function endpoints(): array { return [ - Endpoint\Delete::make() - ->can('delete'), + Endpoint\Endpoint::make('delete_reactions') + ->route('DELETE', '/delete') + ->action(function (Context $context) { + $actor = $context->getActor(); + $data = $context->body(); + + $postId = Arr::get($data, 'postId'); + $isSpecific = Arr::get($data, 'isSpecific'); + $reactionOrPostReactionId = Arr::get($data, 'reactionOrPostReactionId'); + + $post = Post::whereVisibleTo($actor)->findOrFail($postId); + + if (! $isSpecific) { + $reactionId = $reactionOrPostReactionId; + + // Delete all post_reactions of a specific type (i.e. `reaction_id`) + $actor->assertCan('deleteReactions', $post); + + PostReaction::query()->where('post_id', $postId)->where('reaction_id', $reactionId)->delete(); + PostAnonymousReaction::query()->where('post_id', $postId)->where('reaction_id', $reactionId)->delete(); + } elseif ($reactionOrPostReactionId) { + $postReactionId = $reactionOrPostReactionId; + + // Delete a specific post_reaction for the post + /** + * @var PostReaction $reaction + */ + $reaction = PostReaction::query()->where('post_id', $postId)->where('id', $postReactionId)->firstOrFail(); + + // If the post is not the actor's, they must have permission to delete reactions + if ($reaction->user_id != $actor->id) { + $actor->assertCan('deleteReactions', $post); + } else { + $actor->assertCan('react', $post); + } + + $reaction->delete(); + } + }) + ->response(fn (OriginalContext $context) => new EmptyResponse(204)), Endpoint\Index::make() ->paginate(), ]; diff --git a/src/Api/Resource/ReactionResource.php b/src/Api/Resource/ReactionResource.php index 56f8e9e..4b24fdb 100644 --- a/src/Api/Resource/ReactionResource.php +++ b/src/Api/Resource/ReactionResource.php @@ -31,11 +31,6 @@ public function model(): string return Reaction::class; } - public function scope(Builder $query, OriginalContext $context): void - { - $query->whereVisibleTo($context->getActor()); - } - public function endpoints(): array { return [