From 17ff27263e50afc0d126493c143150a6291c46df Mon Sep 17 00:00:00 2001 From: Murtaza Sultani Date: Wed, 5 Nov 2025 12:56:16 +0100 Subject: =?UTF-8?q?Resolve=20"Forum:=20Reactions=20k=C3=B6nnen=20doppelt?= =?UTF-8?q?=20angelegt=20werden"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #6025 Merge request studip/studip!4593 --- ...nique_constraint_to_forum_posting_reactions.php | 27 ++++++++++++++ .../JsonApi/Routes/Forum/PostingReactionStore.php | 42 +++++++++++++++------- .../vue/components/forum/posts/PostReactions.vue | 16 ++++++--- 3 files changed, 67 insertions(+), 18 deletions(-) create mode 100644 db/migrations/6.2.2_add_unique_constraint_to_forum_posting_reactions.php diff --git a/db/migrations/6.2.2_add_unique_constraint_to_forum_posting_reactions.php b/db/migrations/6.2.2_add_unique_constraint_to_forum_posting_reactions.php new file mode 100644 index 0000000..4a73a9f --- /dev/null +++ b/db/migrations/6.2.2_add_unique_constraint_to_forum_posting_reactions.php @@ -0,0 +1,27 @@ +exec(" + DELETE t1 + FROM forum_posting_reactions AS t1 + JOIN forum_posting_reactions AS t2 + ON t1.posting_id = t2.posting_id + AND t1.user_id = t2.user_id + AND t1.emoji = t2.emoji + AND t1.id > t2.id; + "); + + DBManager::get()->exec( + "ALTER TABLE forum_posting_reactions ADD CONSTRAINT unique_posting_user_emoji UNIQUE (posting_id, user_id, emoji)" + ); + } + + protected function down() + { + DBManager::get()->exec("ALTER TABLE forum_posting_reactions DROP INDEX unique_posting_user_emoji"); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/PostingReactionStore.php b/lib/classes/JsonApi/Routes/Forum/PostingReactionStore.php index 992cf0d..60e66c0 100644 --- a/lib/classes/JsonApi/Routes/Forum/PostingReactionStore.php +++ b/lib/classes/JsonApi/Routes/Forum/PostingReactionStore.php @@ -38,23 +38,39 @@ class PostingReactionStore extends JsonApiController throw new AuthorizationFailedException(); } - $posting_reaction = PostingReaction::create([ + $data = [ 'posting_id' => $posting->posting_id, - 'user_id' => $user->user_id, - 'emoji' => self::arrayGet($json, 'data.attributes.emoji') - ]); + 'user_id' => $user->user_id, + 'emoji' => self::arrayGet($json, 'data.attributes.emoji'), + ]; + + $reaction = PostingReaction::findOneBySQL( + "posting_id = :posting_id AND user_id = :user_id AND emoji = :emoji", + $data + ); - if ($user->user_id !== $posting->user_id) { - \PersonalNotifications::add( - $posting->user_id, - \URLHelper::getURL('dispatch.php/course/forum/discussions/show/'.$posting->discussion_id, ['cid' => $posting->range_id], true)."#post_" . $posting->posting_id, - sprintf(_("%s hat auf deinen Beitrag reagiert."), $user->getFullName()), - null, - self::arrayGet($json, 'data.meta.emoji-icon') - ); + if (!$reaction) { + $reaction = PostingReaction::create($data); + + if ($user->user_id !== $posting->user_id) { + \PersonalNotifications::add( + $posting->user_id, + \URLHelper::getURL( + "dispatch.php/course/forum/discussions/show/{$posting->discussion_id}#post_{$posting->posting_id}", + ['cid' => $posting->range_id], + true + ), + studip_interpolate( + _('%{name} hat auf deinen Beitrag reagiert.'), + ['name' => $user->getFullName()] + ), + null, + self::arrayGet($json, 'data.meta.emoji-icon') + ); + } } - return $this->getCreatedResponse($posting_reaction); + return $this->getCreatedResponse($reaction); } protected function validateResourceDocument($json, $data) diff --git a/resources/vue/components/forum/posts/PostReactions.vue b/resources/vue/components/forum/posts/PostReactions.vue index 31643ba..ca5e5a2 100644 --- a/resources/vue/components/forum/posts/PostReactions.vue +++ b/resources/vue/components/forum/posts/PostReactions.vue @@ -27,6 +27,7 @@ const props = defineProps({ const showReactions = ref(false); const reactionStatusMessage = ref(null); +const isLoading = ref(false); const transformedReactions = computed(() => props.reactions.map(reaction => { return { @@ -39,7 +40,7 @@ const groupedReactions = computed(() => Object.groupBy(transformedReactions.valu const announceToScreenReader = message => reactionStatusMessage.value.textContent = message; -const getPostReactionJSONAPIObject = (emoji) => ({ +const getPostReactionJSONAPIObject = emoji => ({ data: { type: 'forum-posting-reactions', attributes: { @@ -57,9 +58,9 @@ const getPostReactionJSONAPIObject = (emoji) => ({ } } } -}) +}); -const storeReaction = async (emoji) => { +const storeReaction = async emoji => { try { const response = await STUDIP.jsonapi.withPromises().POST( 'forum-posting-reactions?include=user&fields[users]=id,username,formatted-name', @@ -69,25 +70,28 @@ const storeReaction = async (emoji) => { const reaction = await deserializeJSONAPIResponse(response); forumDiscussionPost.addPostReaction(reaction, props.posting_id); showReactions.value = false; + return reaction; } catch (error) { STUDIP.Report.error(error); } } -const deleteReaction = async (reactionId) => { +const deleteReaction = async reactionId => { try { await STUDIP.jsonapi.withPromises().DELETE(`forum-posting-reactions/${reactionId}`); forumDiscussionPost.removePostReaction(reactionId, props.posting_id); + return true; } catch (error) { STUDIP.Report.error(error); } } const toggleReaction = async (emoji, reactions = transformedReactions.value) => { - if (forumConfig.allowGuestAccess) { + if (forumConfig.allowGuestAccess || isLoading.value) { return; } + isLoading.value = true; const userReaction = findUserReaction(emoji, reactions); if (userReaction) { @@ -97,6 +101,8 @@ const toggleReaction = async (emoji, reactions = transformedReactions.value) => await storeReaction(emoji); announceToScreenReader($gettext('Reaktion wurde hinzugefügt.')); } + + isLoading.value = false; } const findUserReaction = (emoji, reactions = transformedReactions.value) => reactions.find(reaction => reaction.user.id === STUDIP.USER_ID && reaction.emoji === emoji); -- cgit v1.0