| @@ -4,9 +4,11 @@ | ||
| 4 | 4 | |
| 5 | 5 | use FluentCommunity\App\Models\Comment; |
| 6 | 6 | use FluentCommunity\App\Models\Feed; |
| 7 | 7 | use FluentCommunity\App\Models\Reaction; |
| 8 | +use FluentCommunity\App\Models\XProfile; | |
| 8 | 9 | use FluentCommunity\App\Services\FeedsHelper; |
| 10 | +use FluentCommunity\App\Services\Helper; | |
| 9 | 11 | use FluentCommunity\App\Services\ProfileHelper; |
| 10 | 12 | use FluentCommunity\Framework\Http\Request\Request; |
| 11 | 13 | use FluentCommunity\Framework\Support\Arr; |
| 12 | 14 | |
| @@ -78,17 +80,18 @@ | ||
| 78 | 80 | { |
| 79 | 81 | $currentUser = $this->getUser(true); |
| 80 | 82 | $feed = Feed::withoutGlobalScopes()->byUserAccess($currentUser->ID)->findOrFail($feed_id); |
| 81 | 83 | $type = $request->get('react_type', 'like'); |
| 84 | + $type = in_array($type, ['like', 'bookmark'], true) ? $type : 'like'; | |
| 82 | 85 | $willRemove = $request->get('remove'); |
| 83 | 86 | |
| 84 | - if ($feed->status != 'published') { | |
| 87 | + if (!in_array($feed->status, FeedsHelper::getViewableByLinkStatuses(), true)) { | |
| 85 | 88 | return $this->sendError([ |
| 86 | 89 | 'message' => __('This post is not published yet', 'fluent-community') |
| 87 | 90 | ]); |
| 88 | 91 | } |
| 89 | 92 | |
| 90 | - if ($currentUser->ID === $feed->user_id && apply_filters('fluent_community/disable_self_post_react', false, $feed)) { | |
| 93 | + if (!$willRemove && (int) $currentUser->ID === (int) $feed->user_id && apply_filters('fluent_community/disable_self_post_react', false, $feed)) { | |
| 91 | 94 | return $this->sendError([ |
| 92 | 95 | 'message' => __('You cannot react to your own post', 'fluent-community') |
| 93 | 96 | ]); |
| 94 | 97 | } |
| @@ -122,20 +125,41 @@ | ||
| 122 | 125 | 'new_count' => $feed->reactions_count |
| 123 | 126 | ]; |
| 124 | 127 | } |
| 125 | 128 | |
| 126 | - $react = Reaction::create([ | |
| 127 | - 'user_id' => $currentUser->ID, | |
| 128 | - 'object_id' => $feed->id, | |
| 129 | - 'type' => $type, | |
| 130 | - 'object_type' => 'feed' | |
| 131 | - ]); | |
| 129 | + // Serialize a user's concurrent reactions by locking their profile row, | |
| 130 | + // so parallel add requests cannot each insert a duplicate reaction. The | |
| 131 | + // like counter is updated atomically, so unrelated users never contend. | |
| 132 | + $react = Helper::dbTransaction(function () use ($feed, $currentUser, $type) { | |
| 133 | + XProfile::where('user_id', $currentUser->ID)->lockForUpdate()->first(); | |
| 132 | 134 | |
| 133 | - if ($type == 'like') { | |
| 134 | - $feed->reactions_count = $feed->reactions_count + 1; | |
| 135 | - $feed->timestamps = false; // Don't update the updated_at timestamp | |
| 136 | - $feed->save(); | |
| 135 | + $react = Reaction::where('user_id', $currentUser->ID) | |
| 136 | + ->where('object_id', $feed->id) | |
| 137 | + ->where('type', $type) | |
| 138 | + ->objectType('feed') | |
| 139 | + ->first(); | |
| 137 | 140 | |
| 141 | + if ($react) { | |
| 142 | + return $react; | |
| 143 | + } | |
| 144 | + | |
| 145 | + $react = Reaction::create([ | |
| 146 | + 'user_id' => $currentUser->ID, | |
| 147 | + 'object_id' => $feed->id, | |
| 148 | + 'type' => $type, | |
| 149 | + 'object_type' => 'feed' | |
| 150 | + ]); | |
| 151 | + | |
| 152 | + if ($type == 'like') { | |
| 153 | + // getQuery() so the atomic increment does not touch updated_at | |
| 154 | + Feed::withoutGlobalScopes()->where('id', $feed->id)->getQuery()->increment('reactions_count'); | |
| 155 | + $feed->reactions_count = $feed->reactions_count + 1; | |
| 156 | + } | |
| 157 | + | |
| 158 | + return $react; | |
| 159 | + }); | |
| 160 | + | |
| 161 | + if ($react->wasRecentlyCreated && $type == 'like') { | |
| 138 | 162 | $react->load('xprofile'); |
| 139 | 163 | do_action('fluent_community/feed/react_added', $react, $feed); |
| 140 | 164 | } |
| 141 | 165 | |