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/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/extend.php b/extend.php index ef85134..b385623 100644 --- a/extend.php +++ b/extend.php @@ -11,16 +11,17 @@ 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,43 +36,35 @@ 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) ->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) - ->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), - (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/package.json b/js/package.json index 05eb503..289fc08 100644 --- a/js/package.json +++ b/js/package.json @@ -11,8 +11,8 @@ }, "dependencies": { "@flarum/prettier-config": "^1.0.0", - "flarum-tsconfig": "^1.0.2", - "flarum-webpack-config": "^2.0.0", + "flarum-tsconfig": "^2.0.0", + "flarum-webpack-config": "^3.0.0", "fuzzyset": "^1.0.7", "lodash.debounce": "^4.0.8", "simple-emoji-map": "^0.5.1", 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/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/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..2dd2258 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 })}
  • } ); @@ -113,7 +110,10 @@ 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>(`post_reactions`, { + include: 'user,reaction', + filter: { post: this.attrs.post.id() }, + }); const groupedReactions = groupBy(response, (r: PostReaction) => r.reactionId()); const reactions: ReactionGroup[] = []; @@ -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/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', 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 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; diff --git a/src/Access/ScopePostReactionVisibility.php b/src/Access/ScopePostReactionVisibility.php new file mode 100644 index 0000000..299b94c --- /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 deleted file mode 100644 index 112e366..0000000 --- a/src/Api/Controller/DeletePostReactionController.php +++ /dev/null @@ -1,67 +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/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..dca17e6 --- /dev/null +++ b/src/Api/Resource/PostReactionResource.php @@ -0,0 +1,108 @@ + + */ +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\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(), + ]; + } + + 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..4b24fdb --- /dev/null +++ b/src/Api/Resource/ReactionResource.php @@ -0,0 +1,118 @@ + + */ +class ReactionResource extends Resource\AbstractDatabaseResource +{ + public function type(): string + { + return 'reactions'; + } + + public function model(): string + { + return Reaction::class; + } + + 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..9d04bdf --- /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()->all()), + ]; + } +} diff --git a/src/Notification/PostReactedBlueprint.php b/src/Notification/PostReactedBlueprint.php index 1b817ea..f647fe1 100755 --- a/src/Notification/PostReactedBlueprint.php +++ b/src/Notification/PostReactedBlueprint.php @@ -11,11 +11,13 @@ 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 @@ -46,7 +48,7 @@ public function __construct(Post $post, User $user, string $reaction) /** * {@inheritdoc} */ - public static function getType() + public static function getType(): string { return 'postReacted'; } @@ -54,7 +56,7 @@ public static function getType() /** * {@inheritdoc} */ - public static function getSubjectModel() + public static function getSubjectModel(): string { return Post::class; } @@ -62,7 +64,7 @@ public static function getSubjectModel() /** * {@inheritdoc} */ - public function getSubject() + public function getSubject(): ?AbstractModel { return $this->post; } @@ -70,7 +72,7 @@ public function getSubject() /** * {@inheritdoc} */ - public function getFromUser() + public function getFromUser(): ?User { return $this->user; } @@ -88,7 +90,7 @@ public function getReactionType() /** * {@inheritdoc} */ - public function getData() + public function getData(): mixed { return $this->reaction; } diff --git a/src/PostAttributes.php b/src/PostAttributes.php deleted file mode 100644 index 5779b8d..0000000 --- a/src/PostAttributes.php +++ /dev/null @@ -1,97 +0,0 @@ -settings = $settings; - } - - public function __invoke(PostSerializer $serializer, Post $post, array $attributes): 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; - } - - protected function getReactionCountsForPost(Post $post): array - { - // Initialize counts array - $counts = []; - - // Query for reactions from registered users - $registeredReactions = PostReaction::where('post_id', $post->id) - ->groupBy('reaction_id') - ->selectRaw('reaction_id, COUNT(*) as count') - ->pluck('count', 'reaction_id'); - - // Query for anonymous reactions if allowed - $anonymousReactions = collect([]); - if ($this->settings->get('fof-reactions.anonymousReactions')) { - $anonymousReactions = PostAnonymousReaction::where('post_id', $post->id) - ->groupBy('reaction_id') - ->selectRaw('reaction_id, COUNT(*) as count') - ->pluck('count', 'reaction_id'); - } - - // Merge the registered and anonymous reactions - $reactions = Reaction::all(); - foreach ($reactions as $reaction) { - $counts[$reaction->id] = $registeredReactions->get($reaction->id, 0) + $anonymousReactions->get($reaction->id, 0); - } - - return $counts; - } - - protected function getActorReactionForPost(User $actor, Post $post, ServerRequestInterface $request): ?int - { - if ($actor->isGuest()) { - $session = $request->getAttribute('session'); - - if ($session === null) { - return null; - } - - return PostAnonymousReaction::where('post_id', $post->id) - ->where('guest_id', $session->getId()) - ->value('reaction_id'); - } - - return PostReaction::where('post_id', $post->id) - ->where('user_id', $actor->id) - ->value('reaction_id'); - } -} diff --git a/src/PostReaction.php b/src/PostReaction.php index 21feffb..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,11 +30,13 @@ */ class PostReaction extends AbstractModel { + use ScopeVisibilityTrait; + protected $table = 'post_reactions'; public $timestamps = true; - public $dates = ['created_at', 'updated_at']; + protected $casts = ['created_at' => 'datetime', 'updated_at' => 'datetime']; public function reaction() { 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/Listener/SaveReactionsToDatabase.php b/src/PostResourceFields.php old mode 100755 new mode 100644 similarity index 65% rename from src/Listener/SaveReactionsToDatabase.php rename to src/PostResourceFields.php index 040b7dc..a3efe07 --- a/src/Listener/SaveReactionsToDatabase.php +++ b/src/PostResourceFields.php @@ -9,12 +9,14 @@ * file that was distributed with this source code. */ -namespace FoF\Reactions\Listener; +namespace FoF\Reactions; +use Flarum\Api\Context; +use Flarum\Api\Schema; use Flarum\Extension\ExtensionManager; use Flarum\Foundation\ValidationException; use Flarum\Likes\Event\PostWasLiked; -use Flarum\Post\Event\Saving; +use Flarum\Locale\TranslatorInterface; use Flarum\Post\Post; use Flarum\Settings\SettingsRepositoryInterface; use Flarum\User\User; @@ -22,60 +24,90 @@ use FoF\Reactions\Event\PostWasReacted; use FoF\Reactions\Event\PostWasUnreacted; use FoF\Reactions\Event\WillReactToPost; -use FoF\Reactions\PostAnonymousReaction; -use FoF\Reactions\PostReaction; -use FoF\Reactions\Reaction; use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Support\Arr; use Psr\Http\Message\ServerRequestInterface; -use Pusher; -use Symfony\Contracts\Translation\TranslatorInterface; -class SaveReactionsToDatabase +class PostResourceFields { - /** - * @var SettingsRepositoryInterface - */ - protected $settings; + public function __construct( + protected SettingsRepositoryInterface $settings, + protected Dispatcher $events, + protected ExtensionManager $extensions, + protected TranslatorInterface $translator + ) { + } - /** - * @var TranslatorInterface - */ - protected $translator; + public function __invoke(): array + { + 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\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(...)), + ]; + } - /** - * @var ExtensionManager - */ - protected $extensions; + protected function getReactionCountsForPost(Post $post): array + { + // Initialize counts array + $counts = []; + + // Query for reactions from registered users + $registeredReactions = PostReaction::where('post_id', $post->id) + ->groupBy('reaction_id') + ->selectRaw('reaction_id, COUNT(*) as count') + ->pluck('count', 'reaction_id'); + + // Query for anonymous reactions if allowed + $anonymousReactions = collect([]); + if ($this->settings->get('fof-reactions.anonymousReactions')) { + $anonymousReactions = PostAnonymousReaction::where('post_id', $post->id) + ->groupBy('reaction_id') + ->selectRaw('reaction_id, COUNT(*) as count') + ->pluck('count', 'reaction_id'); + } - /** - * @var Dispatcher - */ - protected $events; + // Merge the registered and anonymous reactions + $reactions = Reaction::all(); + foreach ($reactions as $reaction) { + $counts[$reaction->id] = $registeredReactions->get($reaction->id, 0) + $anonymousReactions->get($reaction->id, 0); + } - public function __construct(SettingsRepositoryInterface $settings, TranslatorInterface $translator, ExtensionManager $extensions, Dispatcher $events) - { - $this->settings = $settings; - $this->translator = $translator; - $this->extensions = $extensions; - $this->events = $events; + return $counts; } - /** - * @param Saving $event - * - * @throws \Flarum\User\Exception\PermissionDeniedException - * @throws \Flarum\Foundation\ValidationException - */ - public function handle(Saving $event) + protected function getActorReactionForPost(User $actor, Post $post, ServerRequestInterface $request): ?int { - $post = $event->post; - $data = $event->data; + if ($actor->isGuest()) { + $session = $request->getAttribute('session'); + + if ($session === null) { + return null; + } + + return PostAnonymousReaction::where('post_id', $post->id) + ->where('guest_id', $session->getId()) + ->value('reaction_id'); + } - if ($post->exists && Arr::has($data, 'attributes.reaction')) { - $actor = $event->actor; + return PostReaction::where('post_id', $post->id) + ->where('user_id', $actor->id) + ->value('reaction_id'); + } - $reactionId = Arr::get($data, 'attributes.reaction'); + protected function setReaction(Post $post, ?string $reactionId, Context $context): void + { + if ($post->exists) { + $actor = $context->getActor(); $actor->assertCan('react', $post); @@ -83,8 +115,8 @@ public function handle(Saving $event) $this->events->dispatch(new WillReactToPost($post, $actor, $reaction)); - $gamification = $this->isExtensionEnabled('fof-gamification'); - $likes = $this->isExtensionEnabled('flarum-likes'); + $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'); @@ -121,12 +153,14 @@ public function handle(Saving $event) $post->raise(new PostWasLiked($post, $actor)); } } else { - $guestId = $this->getSessionId(); + $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) { @@ -220,18 +254,4 @@ protected function validateReaction($reactionId) ]); } } - - 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/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', - ], - ]; -} 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');