| @@ -4,11 +4,14 @@ | ||
| 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; |
| 13 | +use FluentCommunity\Framework\Support\Arr; | |
| 11 | 14 | |
| 12 | 15 | class ReactionController extends Controller |
| 13 | 16 | { |
| 14 | 17 | public function getByFeedId(Request $request) |
| @@ -29,14 +32,16 @@ | ||
| 29 | 32 | 'xprofile' => function ($q) { |
| 30 | 33 | $q->select(ProfileHelper::getXProfilePublicFields()); |
| 31 | 34 | } |
| 32 | 35 | ]) |
| 36 | + ->where('type', 'like') | |
| 37 | + ->distinct('user_id') | |
| 33 | 38 | ->limit(100) |
| 34 | 39 | ->get(); // Todo: Add lazy loading in the future |
| 35 | 40 | |
| 36 | - return [ | |
| 41 | + return apply_filters('fluent_community/reactions_api_response', [ | |
| 37 | 42 | 'reactions' => $reactions |
| 38 | - ]; | |
| 43 | + ], $reactions, $request->all()); | |
| 39 | 44 | } |
| 40 | 45 | |
| 41 | 46 | public function getByCommentId(Request $request) |
| 42 | 47 | { |
| @@ -50,9 +55,9 @@ | ||
| 50 | 55 | |
| 51 | 56 | $comment = Comment::findOrFail($commentId); |
| 52 | 57 | |
| 53 | 58 | // Just validate the permission |
| 54 | - $feed = Feed::withoutGlobalScopes()->byUserAccess(get_current_user_id())->findOrFail($comment->post_id); | |
| 59 | + Feed::withoutGlobalScopes()->byUserAccess(get_current_user_id())->findOrFail($comment->post_id); | |
| 55 | 60 | |
| 56 | 61 | $reactions = $comment |
| 57 | 62 | ->reactions() |
| 58 | 63 | ->whereHas('xprofile') |
| @@ -60,23 +65,39 @@ | ||
| 60 | 65 | 'xprofile' => function ($q) { |
| 61 | 66 | $q->select(ProfileHelper::getXProfilePublicFields()); |
| 62 | 67 | } |
| 63 | 68 | ]) |
| 69 | + ->where('type', 'like') | |
| 70 | + ->distinct('user_id') | |
| 64 | 71 | ->limit(100) |
| 65 | 72 | ->get(); // Todo: Add lazy loading in the future |
| 66 | 73 | |
| 67 | - return [ | |
| 74 | + return apply_filters('fluent_community/reactions_api_response', [ | |
| 68 | 75 | 'reactions' => $reactions |
| 69 | - ]; | |
| 76 | + ], $reactions, $request->all()); | |
| 70 | 77 | } |
| 71 | 78 | |
| 72 | 79 | public function addOrRemovePostReact(Request $request, $feed_id) |
| 73 | 80 | { |
| 74 | - $feed = Feed::withoutGlobalScopes()->findOrFail($feed_id); | |
| 81 | + $currentUser = $this->getUser(true); | |
| 82 | + $feed = Feed::withoutGlobalScopes()->byUserAccess($currentUser->ID)->findOrFail($feed_id); | |
| 75 | 83 | $type = $request->get('react_type', 'like'); |
| 84 | + $type = in_array($type, ['like', 'bookmark'], true) ? $type : 'like'; | |
| 76 | 85 | $willRemove = $request->get('remove'); |
| 77 | 86 | |
| 78 | - $react = Reaction::where('user_id', get_current_user_id()) | |
| 87 | + if (!in_array($feed->status, FeedsHelper::getViewableByLinkStatuses(), true)) { | |
| 88 | + return $this->sendError([ | |
| 89 | + 'message' => __('This post is not published yet', 'fluent-community') | |
| 90 | + ]); | |
| 91 | + } | |
| 92 | + | |
| 93 | + if (!$willRemove && (int) $currentUser->ID === (int) $feed->user_id && apply_filters('fluent_community/disable_self_post_react', false, $feed)) { | |
| 94 | + return $this->sendError([ | |
| 95 | + 'message' => __('You cannot react to your own post', 'fluent-community') | |
| 96 | + ]); | |
| 97 | + } | |
| 98 | + | |
| 99 | + $react = Reaction::where('user_id', $currentUser->ID) | |
| 79 | 100 | ->where('object_id', $feed->id) |
| 80 | 101 | ->where('type', $type) |
| 81 | 102 | ->objectType('feed') |
| 82 | 103 | ->first(); |
| @@ -83,17 +104,18 @@ | ||
| 83 | 104 | |
| 84 | 105 | if ($willRemove) { |
| 85 | 106 | if ($react) { |
| 86 | 107 | $react->delete(); |
| 87 | - | |
| 88 | 108 | if ($type == 'like') { |
| 89 | 109 | $feed->reactions_count = $feed->reactions_count - 1; |
| 110 | + $feed->timestamps = false; // Don't update the updated_at timestamp | |
| 90 | 111 | $feed->save(); |
| 112 | + do_action('fluent_community/feed/react_removed', $feed); | |
| 91 | 113 | } |
| 92 | 114 | } |
| 93 | 115 | |
| 94 | 116 | return [ |
| 95 | - 'message' => 'Reaction has been removed', | |
| 117 | + 'message' => __('Reaction has been removed', 'fluent-community'), | |
| 96 | 118 | 'new_count' => $feed->reactions_count |
| 97 | 119 | ]; |
| 98 | 120 | } |
| 99 | 121 | |
| @@ -98,30 +120,52 @@ | ||
| 98 | 120 | } |
| 99 | 121 | |
| 100 | 122 | if ($react) { |
| 101 | 123 | return [ |
| 102 | - 'message' => 'You have already reacted to this post', | |
| 124 | + 'message' => __('You have already reacted to this post', 'fluent-community'), | |
| 103 | 125 | 'new_count' => $feed->reactions_count |
| 104 | 126 | ]; |
| 105 | 127 | } |
| 106 | 128 | |
| 107 | - $react = Reaction::create([ | |
| 108 | - 'user_id' => get_current_user_id(), | |
| 109 | - 'object_id' => $feed->id, | |
| 110 | - 'type' => $type, | |
| 111 | - 'object_type' => 'feed' | |
| 112 | - ]); | |
| 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(); | |
| 113 | 134 | |
| 114 | - if ($type == 'like') { | |
| 115 | - $feed->reactions_count = $feed->reactions_count + 1; | |
| 116 | - $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(); | |
| 117 | 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') { | |
| 118 | 162 | $react->load('xprofile'); |
| 119 | 163 | do_action('fluent_community/feed/react_added', $react, $feed); |
| 120 | 164 | } |
| 121 | 165 | |
| 122 | 166 | return [ |
| 123 | - 'message' => 'Reaction has been added', | |
| 167 | + 'message' => __('Reaction has been added', 'fluent-community'), | |
| 124 | 168 | 'new_count' => $feed->reactions_count |
| 125 | 169 | ]; |
| 126 | 170 | } |
| 127 | 171 | |
| @@ -126,30 +170,55 @@ | ||
| 126 | 170 | } |
| 127 | 171 | |
| 128 | 172 | public function castSurveyVote(Request $request, $feed_id) |
| 129 | 173 | { |
| 130 | - $feed = Feed::where('id', $feed_id) | |
| 131 | - ->byUserAccess($this->getUserId()) | |
| 174 | + $userId = $this->getUserId(); | |
| 175 | + | |
| 176 | + $feed = Feed::byUserAccess($userId) | |
| 177 | + ->where('id', $feed_id) | |
| 132 | 178 | ->first(); |
| 133 | 179 | |
| 134 | - if (!$feed || $feed->content_type != 'survey') { | |
| 180 | + if (!$feed || $feed->content_type != 'survey' || !$userId) { | |
| 135 | 181 | return $this->sendError([ |
| 136 | 182 | 'message' => __('Sorry! you do not have access to this post or invalid request', 'fluent-community') |
| 137 | 183 | ]); |
| 138 | 184 | } |
| 139 | 185 | |
| 140 | - $voteIndexes = $request->get('vote_indexes', []); | |
| 186 | + $surveyConfig = Arr::get($feed->meta, 'survey_config', []); | |
| 187 | + if (empty($surveyConfig['options'])) { | |
| 188 | + return $this->sendError([ | |
| 189 | + 'message' => __('Sorry! This survey configuration is invalid', 'fluent-community') | |
| 190 | + ]); | |
| 191 | + } | |
| 141 | 192 | |
| 142 | - $feed = FeedsHelper::castSurveyVote($voteIndexes, $feed, $this->getUserId()); | |
| 143 | - $surveyConfig = $feed->meta['survey_config']; | |
| 144 | - $votedOptions = $feed->getSurveyCastsByUserId($this->getUserId()); | |
| 193 | + $endDate = Arr::get($surveyConfig, 'end_date'); | |
| 194 | + if ($endDate && strtotime($endDate) < current_time('timestamp')) { | |
| 195 | + return $this->sendError([ | |
| 196 | + 'message' => __('Sorry! This survey has ended', 'fluent-community') | |
| 197 | + ]); | |
| 198 | + } | |
| 145 | 199 | |
| 146 | - foreach ($surveyConfig['options'] as $index => $option) { | |
| 147 | - if (in_array($option['slug'], $votedOptions)) { | |
| 200 | + $voteIndexes = (array) $request->get('vote_indexes', []); | |
| 201 | + | |
| 202 | + $voteIndexes = array_values(array_map('sanitize_text_field', $voteIndexes)); | |
| 203 | + | |
| 204 | + $feed = FeedsHelper::castSurveyVote($voteIndexes, $feed, $userId); | |
| 205 | + $surveyConfig = Arr::get($feed->meta, 'survey_config', []); | |
| 206 | + $options = Arr::get($surveyConfig, 'options', []); | |
| 207 | + if (empty($options) || !is_array($options)) { | |
| 208 | + $options = []; | |
| 209 | + } | |
| 210 | + | |
| 211 | + $votedOptions = $feed->getSurveyCastsByUserId($userId); | |
| 212 | + | |
| 213 | + foreach ($options as $index => $option) { | |
| 214 | + if (in_array(Arr::get($option, 'slug'), $votedOptions, true)) { | |
| 148 | 215 | $surveyConfig['options'][$index]['voted'] = true; |
| 149 | 216 | } |
| 150 | 217 | } |
| 151 | 218 | |
| 219 | + $surveyConfig = apply_filters('fluent_community/survey_config_response', $surveyConfig, $feed, $userId); | |
| 220 | + | |
| 152 | 221 | return [ |
| 153 | 222 | 'survey_config' => $surveyConfig |
| 154 | 223 | ]; |
| 155 | 224 | } |
| @@ -170,9 +239,10 @@ | ||
| 170 | 239 | ]) |
| 171 | 240 | ->limit(100) |
| 172 | 241 | ->get(); // Todo: Add lazy loading in the future |
| 173 | 242 | |
| 174 | - return [ | |
| 243 | + $data = [ | |
| 175 | 244 | 'voters' => $voters |
| 176 | 245 | ]; |
| 246 | + return apply_filters('fluent_community/survey_voters_api_response', $data, $this->request->all()); | |
| 177 | 247 | } |
| 178 | 248 | } |