Skip to content

Commit

Permalink
Allow deleting entire reaction types off posts
Browse files Browse the repository at this point in the history
  • Loading branch information
dsevillamartin committed Jan 13, 2024
1 parent 687fc2e commit 514dd15
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 25 deletions.
3 changes: 2 additions & 1 deletion extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@

(new Extend\Routes('api'))
->get('/posts/{id}/reactions', 'post.reactions.index', Controller\ListPostReactionsController::class)
->delete('/posts/{id}/reactions/{reactionId}', 'post.reactions.delete', Controller\DeletePostReactionController::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)
Expand Down
4 changes: 3 additions & 1 deletion js/src/forum/components/PostReactAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export default class PostReactAction extends Component {
const reactionCounts = this.post.reactionCounts();
const canReact = this.post.canReact();

const hasReacted = this.post.userReaction() && reactionCounts[this.post.userReaction()] > 0;

return (
<div style="margin-right: 7px" className="Reactions">
<div className="Reactions--reactions">
Expand Down Expand Up @@ -90,7 +92,7 @@ export default class PostReactAction extends Component {
})}
</div>

{(!Object.keys(this.loading).length || this.loading[null]) && !this.post.userReaction() && canReact && (
{(!Object.keys(this.loading).length || this.loading[null]) && !hasReacted && canReact && (
<div className="Reactions--react">
{this.reactButton()}

Expand Down
64 changes: 48 additions & 16 deletions js/src/forum/components/ReactionsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ interface ReactionGroup {
export default class ReactionsModal extends Modal<ReactionsModalAttrs> {
reactions: ReactionGroup[] = [];
loading: boolean = false;
deleting: Record<string, boolean> = {};

deletingSpecific: Record<string, boolean> = {};
deletingType: Record<string, boolean> = {};

className() {
return 'ReactionsModal Modal--small';
Expand Down Expand Up @@ -55,6 +57,8 @@ export default class ReactionsModal extends Modal<ReactionsModalAttrs> {
<div className="Modal-body">
<ul className="ReactionsModal-list">
{this.reactions.map(({ reaction, users, anonymousCount }) => this.buildReactionSection(reaction, users, anonymousCount))}

{!this.reactions.length && <p>{app.translator.trans('fof-reactions.forum.modal.no_reactions')}</p>}
</ul>
</div>
);
Expand All @@ -72,6 +76,14 @@ export default class ReactionsModal extends Modal<ReactionsModalAttrs> {
<legend>
<ReactionComponent reaction={reaction} className={'ReactionModal-reaction'} />
<label className="ReactionsModal-display">{reaction.display() || reaction.identifier()}</label>
{post.canDeletePostReactions() && (
<Button
icon="fas fa-minus-circle"
className="Button Button--icon Button--link"
loading={this.deletingType[reaction.id()!]}
onclick={this.deletePostReaction.bind(this, false, reaction.id()!)}
/>
)}
</legend>

<hr className="ReactionsModal-delimiter" />
Expand All @@ -86,7 +98,7 @@ export default class ReactionsModal extends Modal<ReactionsModalAttrs> {
<Button
icon="fas fa-minus-circle"
className="Button Button--icon Button--link"
loading={this.deleting[postReactionId]}
loading={this.deletingSpecific[postReactionId]}
onclick={this.deletePostReaction.bind(this, postReactionId, reaction.id()!)}
/>
)}
Expand Down Expand Up @@ -139,32 +151,52 @@ export default class ReactionsModal extends Modal<ReactionsModalAttrs> {
m.redraw();
}

async deletePostReaction(postReactionId: string, reactionId: string): Promise<void> {
if (!postReactionId) return;
async deletePostReaction(postReactionId: string | false, reactionId: string): Promise<void> {
const isSpecific = postReactionId !== false;
const loadingArr = isSpecific ? this.deletingSpecific : this.deletingType;
const id = isSpecific ? postReactionId : reactionId;

this.deleting[postReactionId] = true;
loadingArr[id] = true;

await app.request({
method: 'DELETE',
url: `${app.forum.attribute('apiUrl')}/posts/${this.attrs.post.id()}/reactions/${postReactionId}`,
url: `${app.forum.attribute('apiUrl')}/posts/${this.attrs.post.id()}/reactions/${isSpecific ? 'specific' : 'type'}/${id}`,
});

// Filter out the deleted reaction
// Filter out the deleted reaction type
const reaction = this.reactions.find((reaction) => reaction.reaction.id() === reactionId);
const postReaction = app.store.getById('post_reactions', postReactionId);

if (reaction) {
delete reaction.users[postReactionId];
if (isSpecific) {
// Remove only the specific post_reaction
const postReaction = app.store.getById('post_reactions', postReactionId);

if (reaction) {
delete reaction.users[postReactionId];

// Remove reaction group if there are no more reactions of this type
if (!Object.keys(reaction.users).length && !reaction.anonymousCount) {
this.reactions = this.reactions.filter((r) => r.reaction.id() !== reactionId);
// Remove reaction group if there are no more reactions of this type
if (!Object.keys(reaction.users).length && !reaction.anonymousCount) {
this.reactions = this.reactions.filter((r) => r.reaction.id() !== reactionId);
}
}
}

if (postReaction) app.store.remove(postReaction);
if (postReaction) app.store.remove(postReaction);

this.attrs.post.reactionCounts()[reactionId]--;
} else {
// Remove all reactions of this type
this.reactions = this.reactions.filter((r) => r.reaction.id() !== reactionId);

if (reaction) {
for (const postReactionId in reaction.users) {
const postReaction = app.store.getById('post_reactions', postReactionId);
if (postReaction) app.store.remove(postReaction);
}
}

this.attrs.post.reactionCounts()[reactionId] = 0;
}

delete this.deleting[postReactionId];
delete loadingArr[id];

m.redraw();
}
Expand Down
12 changes: 12 additions & 0 deletions resources/less/forum.less
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,18 @@
.ReactionsModal-group {
margin-bottom: 40px;

legend {
display: flex;

label {
flex: 1;
}

.Button {
padding: 2px 0;
}
}

.ReactionsModal-reaction {
.emoji {
height: 24px;
Expand Down
1 change: 1 addition & 0 deletions resources/locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ fof-reactions:
modal:
title: Reactions
anonymous_count: "{count, plural, one {# anonymous user} other {# anonymous users}}"
no_reactions: No reactions yet
mod_item: View Reactions
react_button_label: React

Expand Down
34 changes: 27 additions & 7 deletions src/Api/Controller/DeletePostReactionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,50 @@
use Flarum\Api\Controller\AbstractDeleteController;
use Flarum\Http\RequestUtil;
use Flarum\Post\Post;
use Flarum\User\Exception\PermissionDeniedException;
use FoF\Reactions\PostAnonymousReaction;
use FoF\Reactions\PostReaction;
use Illuminate\Support\Arr;
use Laminas\Diactoros\Response\EmptyResponse;
use Psr\Http\Message\ServerRequestInterface;

class DeletePostReactionController extends AbstractDeleteController
{
protected function delete(ServerRequestInterface $request)
/**
* @throws PermissionDeniedException
*/
protected function delete(ServerRequestInterface $request): EmptyResponse
{
$actor = RequestUtil::getActor($request);
$postId = Arr::get($request->getQueryParams(), 'id');
$postReactionId = Arr::get($request->getQueryParams(), 'reactionId');
$params = $request->getQueryParams();

$postId = Arr::get($params, 'id');
$postReactionId = Arr::get($params, 'postReactionId');
$reactionId = Arr::get($params, 'reactionId');

$post = Post::whereVisibleTo($actor)->findOrFail($postId);
$reaction = PostReaction::query()->where('post_id', $postId)->where('id', $postReactionId)->firstOrFail();

$actor->assertCan('react', $post);

// If the post is not the actor's, they must have permission to delete reactions
if ($reaction->user_id !== $actor->id) {
if ($reactionId) {
// Delete all post_reactions of a specific type (i.e. `reaction_id`)
$actor->assertCan('deletePostReactions', $post);

PostReaction::query()->where('post_id', $postId)->where('reaction_id', $reactionId)->delete();
PostAnonymousReaction::query()->where('post_id', $postId)->where('reaction_id', $reactionId)->delete();
} else if ($postReactionId) {
// Delete a specific post_reaction for the post
$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('deletePostReactions', $post);
}

$reaction->delete();
}

$reaction->delete();
// TODO should this send pusher updates? would need new type for non-specific, otherwise could spam pusher events

return new EmptyResponse(204);
}
Expand Down

0 comments on commit 514dd15

Please sign in to comment.