PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.10.01
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.10.01
2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 1.1.0 All 77 releases
← All changes | app/Http/Controllers/CommentsController.php +391 -114 1.0.972.10.01 View file →
@@ -11,8 +11,9 @@
11 11 use FluentCommunity\Framework\Http\Request\Request;
12 12 use FluentCommunity\App\Models\Comment;
13 13 use FluentCommunity\App\Models\Feed;
14 14 use FluentCommunity\App\Models\Reaction;
15 +use FluentCommunity\App\Models\XProfile;
15 16 use FluentCommunity\Framework\Support\Arr;
16 17
17 18 class CommentsController extends Controller
18 19 {
@@ -17,11 +18,29 @@
17 18 class CommentsController extends Controller
18 19 {
19 20 public function getComments(Request $request, $feed_id)
20 21 {
21 - $feed = Feed::withoutGlobalScopes()->findOrFail($feed_id);
22 - $canViewComments = apply_filters('fluent_community/can_view_comments_' . $feed->type, true, $feed);
22 + $feed = Feed::withoutGlobalScopes()
23 + ->byUserAccess(get_current_user_id())
24 + ->findOrFail($feed_id);
23 25
26 + if (!in_array($feed->status, FeedsHelper::getViewableByLinkStatuses(), true) && !$feed->hasEditAccess($this->getUserId())) {
27 + return $this->sendError([
28 + 'message' => __('Sorry, you do not have permission to view this post', 'fluent-community')
29 + ], 404);
30 + }
31 +
32 + /*
33 + * The row's own setting is the default the filter gets handed, rather than a bare
34 + * true. Before this, meta.enable_comments was read nowhere on this path, so a page
35 + * with comments switched off still served its thread to anyone who asked for it.
36 + */
37 + $canViewComments = apply_filters(
38 + 'fluent_community/can_view_comments_' . $feed->type,
39 + FeedsHelper::commentsEnabled($feed),
40 + $feed
41 + );
42 +
24 43 if (!$canViewComments) {
25 44 return [
26 45 'comments' => []
27 46 ];
@@ -27,8 +46,9 @@
27 46 ];
28 47 }
29 48
30 49 $comments = Comment::where('post_id', $feed->id)
50 + ->byContentModerationAccessStatus($this->getUser())
31 51 ->orderBy('created_at', 'asc')
32 52 ->with([
33 53 'xprofile' => function ($q) {
34 54 $q->select(ProfileHelper::getXProfilePublicFields());
@@ -33,15 +53,19 @@
33 53 'xprofile' => function ($q) {
34 54 $q->select(ProfileHelper::getXProfilePublicFields());
35 55 }
36 56 ])
57 + ->whereHas('xprofile', function ($q) {
58 + $q->where('status', 'active');
59 + })
37 60 ->get();
38 61
62 + $comments = apply_filters('fluent_community/comments_query_response', $comments, $request->all());
63 +
39 64 $userId = $this->getUserId();
40 65
41 66 if ($userId) {
42 67 $likedIds = FeedsHelper::getLikedIdsByUserFeedId($feed->id, get_current_user_id());
43 -
44 68 if ($likedIds) {
45 69 $comments->each(function ($comment) use ($likedIds) {
46 70 if (in_array($comment->id, $likedIds)) {
47 71 $comment->liked = 1;
@@ -49,11 +73,13 @@
49 73 });
50 74 }
51 75 }
52 76
53 - return [
54 - 'comments' => $comments
77 + $data = [
78 + 'comments' => $comments
55 79 ];
80 +
81 + return apply_filters('fluent_community/comments_api_response', $data, $request->all());
56 82 }
57 83
58 84 public function store(Request $request, $feedId)
59 85 {
@@ -62,30 +88,26 @@
62 88
63 89 $text = $this->validateCommentText($request->all());
64 90 $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
65 91
92 + if (!in_array($feed->status, FeedsHelper::getViewableByLinkStatuses(), true)) {
93 + return $this->sendError([
94 + 'message' => __('This post is not published yet', 'fluent-community')
95 + ]);
96 + }
97 +
66 98 $this->verifyCreateCommentPermission($feed);
67 99
68 100 $requestData = $request->all();
69 101
70 - // Check for duplicate
71 - $exist = Comment::where('user_id', get_current_user_id())
72 - ->where('message', $text)
73 - ->where('post_id', $feed->id)
74 - ->first();
102 + [$markdown, $inlineMedias] = FeedsHelper::replaceImageUrlsWithRealMediaArchive($text);
103 + $mentions = FeedsHelper::getMentions($markdown, $feed->space_id, true);
104 + $commentHtml = $this->generateCommentHtml($markdown, $mentions);
75 105
76 - if ($exist) {
77 - return $this->sendError([
78 - 'message' => __('No duplicate comment please!', 'fluent-community')
79 - ]);
80 - }
81 -
82 - $mentions = FeedsHelper::getMentions($text, $feed->space_id);
83 - $commentHtml = $this->generateCommentHtml($text, $mentions);
84 106 $commentData = $this->prepareCommentData($feed->id, $text, $commentHtml);
85 107
86 - if ($parentId = $request->get('parent_id')) {
87 - $parentId = (int) $parentId;
108 + if (!empty($requestData['parent_id'])) {
109 + $parentId = (int)$requestData['parent_id'];
88 110 $parentComment = Comment::where('id', $parentId)
89 111 ->where('post_id', $feed->id)
90 112 ->first();
91 113
@@ -97,35 +119,82 @@
97 119
98 120 $commentData['parent_id'] = $parentId;
99 121 }
100 122
101 - [$commentData, $media] = $this->prepareCommentMedia($commentData, $requestData);
123 + [$commentData, $mediaItems] = $this->prepareCommentMedia($commentData, $requestData);
102 124
125 + $commentData['is_admin'] = $user->hasSpacePermission('community_moderator', $feed->space);
126 +
127 + if ($mentionUserIds = Arr::get($mentions, 'user_ids', [])) {
128 + $commentData['meta']['mentioned_user_ids'] = $mentionUserIds;
129 + }
130 +
103 131 do_action('fluent_community/before_comment_create', $commentData, $feed);
104 132
105 - $commentData = apply_filters('fluent_community/comment/comment_data', $commentData, $feed, $requestData);
133 + $commentData = apply_filters('fluent_community/comment/comment_data', $commentData, $feed);
106 134
107 - $comment = Comment::create($commentData);
135 + // Only comments with text are duplicate checked
136 + $shouldCheckDuplicate = $text && !apply_filters('fluent_community/disable_duplicate_comment_check', false, get_current_user_id(), $feed->id);
108 137
138 + // Serialize a member's concurrent submissions by locking their profile row,
139 + // so parallel matching requests cannot pass the duplicate check and both insert.
140 + $comment = Helper::dbTransaction(function () use ($commentData, $feed, $text, $shouldCheckDuplicate) {
141 + XProfile::where('user_id', get_current_user_id())->lockForUpdate()->first();
142 +
143 + if ($shouldCheckDuplicate && Comment::where('user_id', get_current_user_id())->where('message', $text)->where('post_id', $feed->id)->first()) {
144 + return null;
145 + }
146 +
147 + $newComment = Comment::create($commentData);
148 + Feed::withoutGlobalScopes()->where('id', $feed->id)->increment('comments_count');
149 +
150 + return $newComment;
151 + });
152 +
153 + if (!$comment) {
154 + return $this->sendError([
155 + 'message' => __('No duplicate comment please!', 'fluent-community')
156 + ]);
157 + }
158 +
109 159 $feed->comments_count = $feed->comments_count + 1;
110 - $feed->save();
111 160
112 - if ($media) {
113 - $media->fill([
114 - 'is_active' => 1,
115 - 'feed_id' => $feed->id,
116 - 'object_source' => 'comment',
117 - 'sub_object_id' => $comment->id
118 - ]);
119 - $media->save();
161 +
162 + // Merge and save all media in one loop
163 + $mediaItems = $mediaItems ? (is_array($mediaItems) ? $mediaItems : [$mediaItems]) : [];
164 +
165 + if ($inlineMedias) {
166 + $mediaItems = array_merge($mediaItems, $inlineMedias);
120 167 }
121 168
169 + if ($mediaItems) {
170 + foreach ($mediaItems as $media) {
171 + $media->fill([
172 + 'is_active' => 1,
173 + 'feed_id' => $feed->id,
174 + 'object_source' => 'comment',
175 + 'sub_object_id' => $comment->id
176 + ]);
177 + $media->save();
178 + }
179 + }
180 +
122 181 $this->loadCommentRelations($comment);
123 182
124 - $mentionedUsers = $mentions ? $mentions['users'] : null;
125 - do_action('fluent_community/comment_added_' . $feed->type, $comment, $feed, $mentionedUsers);
126 - do_action('fluent_community/comment_added', $comment, $feed, $mentionedUsers);
183 + if ($comment->status != 'published') {
184 + do_action('fluent_community/comment/new_comment_' . $comment->status, $comment, $feed);
185 + /* translators: %$s is replaced by the status of the comment */
186 + $message = sprintf(__('Your comment has been marked as %s', 'fluent-community'), $comment->status);
187 + $response = [
188 + 'comment' => $comment,
189 + 'message' => $message
190 + ];
191 + return apply_filters('fluent_community/comment/new_comment_response', $response, $comment);
192 + }
127 193
194 + do_action('fluent_community/comment_added_' . $feed->type, $comment, $feed);
195 + do_action('fluent_community/comment_added', $comment, $feed, Arr::get($mentions, 'users', []));
196 +
128 197 return [
129 198 'comment' => $comment,
130 199 'message' => __('Comment has been added', 'fluent-community'),
131 200 ];
@@ -133,29 +202,42 @@
133 202
134 203 public function update(Request $request, $feedId, $commentId)
135 204 {
136 205 $text = $this->validateCommentText($request->all());
206 +
137 207 $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
138 208 $this->verifySpacePermission($feed);
139 209
140 210 $requestData = $request->all();
141 211 $comment = Comment::findOrFail($commentId);
142 - $user = User::find(get_current_user_id());
143 212
144 - if (!$user->can('edit_any_comment') && $comment->user_id != get_current_user_id()) {
213 + if ($comment->post_id != $feed->id) {
145 214 return $this->sendError([
215 + 'message' => __('Invalid comment', 'fluent-community')
216 + ]);
217 + }
218 +
219 + $user = $this->getUser(true);
220 +
221 + $requestData['is_admin'] = $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
222 +
223 + if ($comment->user_id != get_current_user_id() && !$user->can('edit_any_comment', $feed->space)) {
224 + return $this->sendError([
146 225 'message' => __('You are not allowed to edit this comment', 'fluent-community')
147 226 ]);
148 227 }
149 228
150 - $mentions = FeedsHelper::getMentions($text, $feed->space_id);
151 - $commentHtml = $this->generateCommentHtml($text, $mentions);
229 + [$markdown, $inlineMedias] = FeedsHelper::replaceImageUrlsWithRealMediaArchive($text, $feed);
152 230
231 + $mentions = FeedsHelper::getMentions($markdown, $feed->space_id);
232 +
233 + $commentHtml = $this->generateCommentHtml($markdown, $mentions);
234 +
153 235 $commentData = $this->prepareCommentData($feed->id, $text, $commentHtml);
154 236
155 - [$commentData, $media] = $this->prepareCommentMedia($commentData, $requestData, $comment);
237 + [$commentData, $mediaItems] = $this->prepareCommentMedia($commentData, $requestData, $comment);
156 238
157 - $commentData = apply_filters('fluent_community/comment/update_comment_data', $commentData, $feed, $requestData);
239 + $commentData = apply_filters('fluent_community/comment/update_comment_data', $commentData, $feed, $requestData, $comment);
158 240
159 241 $comment->fill($commentData);
160 242
161 243 $dirty = $comment->getDirty();
@@ -163,35 +245,40 @@
163 245 if ($dirty) {
164 246 $comment->save();
165 247 }
166 248
167 - if ($media) {
168 - $media->fill([
169 - 'is_active' => 1,
170 - 'feed_id' => $feed->id,
171 - 'object_source' => 'comment',
172 - 'sub_object_id' => $comment->id
173 - ]);
174 - $media->save();
249 + // Merge and save all media in one loop
250 + $mediaItems = $mediaItems ? (is_array($mediaItems) ? $mediaItems : [$mediaItems]) : [];
175 251
176 - // remove other media
177 - $otherMedias = Media::where('object_source', 'comment')
178 - ->where('sub_object_id', $comment->id)
179 - ->where('id', '!=', $media->id)
180 - ->get();
252 + if ($inlineMedias) {
253 + $mediaItems = array_merge($mediaItems, $inlineMedias);
254 + }
181 255
182 - if (!$otherMedias->isEmpty()) {
183 - // do_action('fluent_community/comment/media_deleted', $otherMedias);
256 + $allMediaIds = [];
257 +
258 + if ($mediaItems) {
259 + foreach ($mediaItems as $media) {
260 + $media->fill([
261 + 'is_active' => 1,
262 + 'feed_id' => $feed->id,
263 + 'object_source' => 'comment',
264 + 'sub_object_id' => $comment->id
265 + ]);
266 + $media->save();
267 + $allMediaIds[] = $media->id;
184 268 }
185 - } else {
186 - // remove other media
187 - $otherMedias = Media::where('object_source', 'comment')
188 - ->where('sub_object_id', $comment->id)
189 - ->get();
269 + }
190 270
191 - if (!$otherMedias->isEmpty()) {
192 - // do_action('fluent_community/comment/media_deleted', $otherMedias);
193 - }
271 + // Remove old media not in current list
272 + $otherMedias = Media::where('object_source', 'comment')
273 + ->when($allMediaIds, function ($q) use ($allMediaIds) {
274 + $q->whereNotIn('id', $allMediaIds);
275 + })
276 + ->where('sub_object_id', $comment->id)
277 + ->get();
278 +
279 + if (!$otherMedias->isEmpty()) {
280 + do_action('fluent_community/comment/media_deleted', $otherMedias);
194 281 }
195 282
196 283 $this->loadCommentRelations($comment);
197 284
@@ -205,32 +292,125 @@
205 292 'message' => __('Comment has been updated', 'fluent-community'),
206 293 ];
207 294 }
208 295
296 + public function patchComment(Request $request, $feedId, $commentId)
297 + {
298 + $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
299 +
300 + $comment = Comment::findOrFail($commentId);
301 +
302 + if ($comment->post_id != $feed->id) {
303 + return $this->sendError([
304 + 'message' => __('Invalid comment', 'fluent-community')
305 + ]);
306 + }
307 +
308 + $user = $this->getUser(true);
309 +
310 + $isMod = $user && $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
311 + $isAdmin = $user && $user->hasPermissionOrInCurrentSpace('community_admin', $feed->space);
312 +
313 + if (!$isMod && !$isAdmin) {
314 + return $this->sendError([
315 + 'message' => __('You do not have permission to perform this action', 'fluent-community')
316 + ]);
317 + }
318 +
319 + $allData = $request->all();
320 + $validKeys = ['is_sticky'];
321 +
322 + $data = Arr::only($allData, $validKeys);
323 +
324 + $data = array_map('intval', $data);
325 +
326 + if (isset($data['is_sticky'])) {
327 + if ($comment->parent_id) {
328 + return $this->sendError([
329 + 'message' => __('You cannot pin a reply comment', 'fluent-community')
330 + ]);
331 + }
332 +
333 + $data['is_sticky'] = $data['is_sticky'] ? 1 : 0;
334 + if ($data['is_sticky']) {
335 + Comment::where('post_id', $feed->id)->update(['is_sticky' => 0]);
336 + }
337 + }
338 +
339 + if ($data) {
340 + $comment->fill($data);
341 + $dirty = $comment->getDirty();
342 + if ($dirty) {
343 + $comment->save();
344 + do_action('fluent_community/comment/updated', $comment, $dirty);
345 + }
346 + }
347 +
348 + return apply_filters('fluent_community/comment/patch_comment_response', [
349 + 'comment' => $comment,
350 + 'message' => __('Comment updated', 'fluent-community')
351 + ], $comment, $feed, $request->all());
352 + }
353 +
209 354 private function prepareCommentMedia($commentData, $requestData, $exisitngComment = null)
210 355 {
211 356 $mediaImages = Arr::get($requestData, 'media_images', []);
212 357
213 358 if ($mediaImages) {
359 + if ($exisitngComment) {
360 + $mediaItems = [];
361 + $mediaData = [];
362 + foreach ($mediaImages as $mediaImage) {
363 + $id = Arr::get($mediaImage, 'media_id');
364 + if ($id) {
365 + $media = Media::where('sub_object_id', $exisitngComment->id)
366 + ->where('object_source', 'comment')
367 + ->find($id);
368 + } else {
369 + $media = Helper::getMediaFromUrl($mediaImage);
370 + }
371 +
372 + if ($media) {
373 + $mediaItems[] = $media;
374 + $mediaData[] = [
375 + 'media_id' => $media->id,
376 + 'url' => $media->public_url,
377 + 'type' => 'image',
378 + 'width' => Arr::get($media->settings, 'width'),
379 + 'height' => Arr::get($media->settings, 'height'),
380 + 'provider' => Arr::get($media->settings, 'provider', 'uploader')
381 + ];
382 + }
383 + }
384 + $commentData['meta']['media_items'] = $mediaData;
385 + return [$commentData, $mediaItems];
386 + }
387 +
214 388 $uploadedImages = Helper::getMediaByProvider($mediaImages);
215 389 if ($uploadedImages) {
216 390 $mediaItems = Helper::getMediaItemsFromUrl($uploadedImages);
217 391 if ($mediaItems) {
218 - $firstMedia = $mediaItems[0];
219 - $commentData['meta']['media_preview'] = [
220 - 'image' => $firstMedia->public_url,
221 - 'type' => 'image',
222 - 'provider' => 'upload',
223 - 'height' => $firstMedia->settings ? Arr::get($firstMedia->settings, 'height', 0) : 0,
224 - 'width' => $firstMedia->settings ? Arr::get($firstMedia->settings, 'width', 0) : 0,
225 - ];
226 - return [$commentData, $firstMedia];
392 + $mediaPreviews = [];
393 + foreach ($mediaItems as $mediaItem) {
394 + $mediaData = [
395 + 'media_id' => $mediaItem->id,
396 + 'url' => $mediaItem->public_url,
397 + 'type' => 'image',
398 + 'width' => Arr::get($mediaItem->settings, 'width'),
399 + 'height' => Arr::get($mediaItem->settings, 'height'),
400 + 'provider' => Arr::get($mediaItem->settings, 'provider', 'uploader')
401 + ];
402 +
403 + $mediaPreviews[] = array_filter($mediaData);
404 + }
405 + $commentData['meta']['media_items'] = $mediaPreviews;
406 + return [$commentData, $mediaItems];
227 407 }
228 408 }
229 409 }
230 410
231 411 if (empty($requestData['meta']['media_preview']['image'])) {
232 - return [$commentData, null];
412 + return [$commentData, []];
233 413 }
234 414
235 415 if ($exisitngComment) {
236 416 $image = sanitize_url(Arr::get($requestData, 'meta.media_preview.image', ''));
@@ -240,37 +420,50 @@
240 420 ->first();
241 421
242 422 if ($existingMedia) {
243 423 $commentData['meta'] = $exisitngComment->meta;
244 - return [$commentData, $existingMedia];
424 + return [$commentData, [$existingMedia]];
245 425 }
246 426 }
247 427
428 + // type/provider reach :class bindings and width/height a :style binding in
429 + // _MediaPreview.vue. Neither is an executable sink, but the stored values are
430 + // request-supplied so they are normalised here rather than trusted.
248 431 $commentData['meta']['media_preview'] = array_filter([
249 432 'image' => sanitize_url(Arr::get($requestData, 'meta.media_preview.image', '')),
250 - 'type' => Arr::get($requestData, 'meta.media_preview.type', 'image'),
251 - 'provider' => Arr::get($requestData, 'meta.media_preview.provider', ''),
252 - 'height' => Arr::get($requestData, 'meta.media_preview.height', 0),
253 - 'width' => Arr::get($requestData, 'meta.media_preview.width', 0),
433 + 'type' => sanitize_text_field(Arr::get($requestData, 'meta.media_preview.type', 'image')),
434 + 'provider' => sanitize_text_field(Arr::get($requestData, 'meta.media_preview.provider', '')),
435 + 'height' => (int) Arr::get($requestData, 'meta.media_preview.height', 0),
436 + 'width' => (int) Arr::get($requestData, 'meta.media_preview.width', 0),
254 437 ]);
255 438
256 - return [$commentData, null];
439 + return [$commentData, []];
257 440 }
258 441
259 442 private function validateCommentText($data)
260 443 {
261 - $text = trim(Arr::get($data, 'comment'));
444 + $text = trim((string) Arr::get($data, 'comment', ''));
262 445 $text = CustomSanitizer::unslashMarkdown($text);
263 446
447 + // Decode HTML entities (e.g.,   for space) and strip all whitespace for validation
448 + $textForValidation = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
449 + $textForValidation = preg_replace('/\s+/u', '', $textForValidation);
450 +
264 451 $hasMedia = Arr::get($data, 'media_images', []) || Arr::get($data, 'meta.media_preview.image', false);
265 452
266 - if (!$text && !$hasMedia) {
267 - throw new \Exception(esc_html__('Please provide your reply text', 'fluent-community'), 422);
453 + $isReply = !empty($data['parent_id']);
454 + if (!$textForValidation && !$hasMedia) {
455 + if ($isReply) {
456 + throw new \Exception(esc_html__('Reply cannot be empty.', 'fluent-community'), 422);
457 + } else {
458 + throw new \Exception(esc_html__('Comment cannot be empty.', 'fluent-community'), 422);
459 + }
268 460 }
269 461
270 462 $maxCommentLength = apply_filters('fluent_community/max_comment_char_length', 10000);
271 463 if ($text && strlen($text) > $maxCommentLength) {
272 - throw new \Exception(esc_html__('Comment text is too long', 'fluent-community'), 422);
464 + /* translators: %s is the maximum allowed character count */
465 + throw new \Exception(esc_html(sprintf(__('The comment is too long. Please keep it under %s characters.', 'fluent-community'), number_format($maxCommentLength))), 422);
273 466 }
274 467
275 468 return $text;
276 469 }
@@ -276,9 +469,9 @@
276 469 }
277 470
278 471 private function verifyCreateCommentPermission($feed)
279 472 {
280 - if (Arr::get($feed->meta, 'comments_disabled') === 'yes') {
473 + if (!FeedsHelper::commentsEnabled($feed)) {
281 474 throw new \Exception(esc_html__('Comments are disabled for this post', 'fluent-community'));
282 475 }
283 476
284 477 $this->verifySpacePermission($feed);
@@ -287,9 +480,13 @@
287 480 private function verifySpacePermission($feed)
288 481 {
289 482 if ($feed->space_id && $feed->space) {
290 483 $user = $this->getUser(true);
291 - $user->verifySpacePermission('registered', $feed->space);
484 + $user->verifySpacePermission('can_comment', $feed->space);
485 +
486 + if ($feed->space->type == 'course' && Arr::get($feed->space->settings, 'disable_comments') === 'yes') {
487 + throw new \Exception(esc_html__('Comments are disabled for this course', 'fluent-community'));
488 + }
292 489 }
293 490 }
294 491
295 492 private function generateCommentHtml($text, $mentions)
@@ -320,13 +517,27 @@
320 517 }
321 518
322 519 public function addOrRemovePostReact(Request $request, $feed_id)
323 520 {
324 - $feed = Feed::withoutGlobalScopes()->findOrFail($feed_id);
521 + $userId = get_current_user_id();
522 + $feed = Feed::withoutGlobalScopes()->byUserAccess($userId)->findOrFail($feed_id);
325 523 $type = $request->get('react_type', 'like');
524 + $type = in_array($type, ['like', 'bookmark'], true) ? $type : 'like';
326 525 $willRemove = $request->get('remove');
327 526
328 - $react = Reaction::where('user_id', get_current_user_id())
527 + if (!in_array($feed->status, FeedsHelper::getViewableByLinkStatuses(), true)) {
528 + return $this->sendError([
529 + 'message' => __('This post is not published yet', 'fluent-community')
530 + ]);
531 + }
532 +
533 + if (!$willRemove && (int) $userId === (int) $feed->user_id && apply_filters('fluent_community/disable_self_post_react', false, $feed)) {
534 + return $this->sendError([
535 + 'message' => __('You cannot react to your own post', 'fluent-community')
536 + ]);
537 + }
538 +
539 + $react = Reaction::where('user_id', $userId)
329 540 ->where('object_id', $feed->id)
330 541 ->where('type', $type)
331 542 ->objectType('feed')
332 543 ->first();
@@ -335,14 +546,16 @@
335 546 if ($react) {
336 547 $react->delete();
337 548 if ($type == 'like') {
338 549 $feed->reactions_count = $feed->reactions_count - 1;
550 + $feed->timestamps = false; // Don't update the updated_at timestamp
339 551 $feed->save();
552 + do_action('fluent_community/feed/react_removed', $feed);
340 553 }
341 554 }
342 555
343 556 return [
344 - 'message' => 'Reaction has been removed',
557 + 'message' => __('Reaction has been removed', 'fluent-community'),
345 558 'new_count' => $feed->reactions_count
346 559 ];
347 560 }
348 561
@@ -347,9 +560,9 @@
347 560 }
348 561
349 562 if ($react) {
350 563 return [
351 - 'message' => 'You have already reacted to this post',
564 + 'message' => __('You have already reacted to this post', 'fluent-community'),
352 565 'new_count' => $feed->reactions_count
353 566 ];
354 567 }
355 568
@@ -361,8 +574,9 @@
361 574 ]);
362 575
363 576 if ($type == 'like') {
364 577 $feed->reactions_count = $feed->reactions_count + 1;
578 + $feed->timestamps = false; // Don't update the updated_at timestamp
365 579 $feed->save();
366 580
367 581 $react->load('xprofile');
368 582 do_action('fluent_community/feed/react_added', $react, $feed);
@@ -368,9 +582,9 @@
368 582 do_action('fluent_community/feed/react_added', $react, $feed);
369 583 }
370 584
371 585 return [
372 - 'message' => 'Reaction has been added',
586 + 'message' => __('Reaction has been added', 'fluent-community'),
373 587 'new_count' => $feed->reactions_count
374 588 ];
375 589 }
376 590
@@ -380,16 +594,16 @@
380 594 $comment = Comment::findOrFail($commentId);
381 595
382 596 if ($comment->post_id != $feed->id) {
383 597 return $this->sendError([
384 - 'message' => 'Invalid comment'
598 + 'message' => __('Invalid comment', 'fluent-community')
385 599 ]);
386 600 }
387 601
388 602 $user = User::find(get_current_user_id());
389 - if (!$user->can('delete_any_comment') && $comment->user_id != get_current_user_id()) {
603 + if ($comment->user_id != get_current_user_id() && !$user->can('delete_any_comment', $feed->space)) {
390 604 return $this->sendError([
391 - 'message' => 'You are not allowed to delete this comment'
605 + 'message' => __('You are not allowed to delete this comment', 'fluent-community')
392 606 ]);
393 607 }
394 608
395 609 do_action('fluent_community/before_comment_delete', $comment);
@@ -400,8 +614,9 @@
400 614
401 615 $comment->delete();
402 616
403 617 $feed->comments_count = Comment::where('post_id', $feed->id)->count();
618 + $feed->timestamps = false; // Don't update the updated_at timestamp
404 619 $feed->save();
405 620
406 621 do_action('fluent_community/comment_deleted_' . $feed->type, $commentId, $feed);
407 622 do_action('fluent_community/comment_deleted', $commentId, $feed);
@@ -410,16 +625,16 @@
410 625 'message' => __('Selected comment has been deleted', 'fluent-community')
411 626 ];
412 627 }
413 628
414 - public function toggoleReaction(Request $request, $feedId, $commentId)
629 + public function toggleReaction(Request $request, $feedId, $commentId)
415 630 {
416 - $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
631 + $feed = Feed::withoutGlobalScopes()->byUserAccess(get_current_user_id())->findOrFail($feedId);
417 632 $comment = Comment::findOrFail($commentId);
418 633
419 634 if ($comment->post_id != $feed->id) {
420 635 return $this->sendError([
421 - 'message' => 'Invalid comment'
636 + 'message' => __('Invalid comment', 'fluent-community')
422 637 ]);
423 638 }
424 639
425 640 $user = User::findOrFail(get_current_user_id());
@@ -427,22 +642,40 @@
427 642 if ($feed->space_id) {
428 643 $user->verifySpacePermission('registered', $feed->space);
429 644 }
430 645
646 + $userId = get_current_user_id();
431 647 $reactionState = !!$request->get('state', false);
432 648
649 + if ($reactionState && (int) $userId === (int) $comment->user_id && apply_filters('fluent_community/disable_self_comment_react', false, $feed)) {
650 + return $this->sendError([
651 + 'message' => __('You cannot react to your own comment', 'fluent-community')
652 + ]);
653 + }
654 +
433 655 if ($reactionState) {
434 - // add or update the reaction
435 - $reaction = Reaction::firstOrCreate([
436 - 'user_id' => get_current_user_id(),
437 - 'object_id' => $comment->id,
438 - 'object_type' => 'comment',
439 - 'parent_id' => $feed->id
440 - ]);
656 + // Serialize concurrent reactions on this comment by locking its row,
657 + // so parallel add requests cannot each insert a duplicate reaction.
658 + $reaction = Helper::dbTransaction(function () use ($comment, $feed) {
659 + XProfile::where('user_id', get_current_user_id())->lockForUpdate()->first();
441 660
661 + $reaction = Reaction::firstOrCreate([
662 + 'user_id' => get_current_user_id(),
663 + 'object_id' => $comment->id,
664 + 'object_type' => 'comment',
665 + 'parent_id' => $feed->id
666 + ]);
667 +
668 + if ($reaction->wasRecentlyCreated) {
669 + Comment::where('id', $comment->id)->increment('reactions_count');
670 + $comment->reactions_count = $comment->reactions_count + 1;
671 + }
672 +
673 + return $reaction;
674 + });
675 +
442 676 if ($reaction->wasRecentlyCreated) {
443 - $comment->reactions_count = $comment->reactions_count + 1;
444 - $comment->save();
677 + do_action('fluent_community/comment/react_added', $reaction, $comment, $feed);
445 678 }
446 679 } else {
447 680 // remove the reaction
448 681 $deleted = Reaction::where('user_id', get_current_user_id())
@@ -452,13 +685,14 @@
452 685
453 686 if ($deleted) {
454 687 $comment->reactions_count = $comment->reactions_count - 1;
455 688 $comment->save();
689 + do_action('fluent_community/comment/react_removed', $comment, $feed);
456 690 }
457 691 }
458 692
459 693 return [
460 - 'message' => 'Reaction has been toggled',
694 + 'message' => __('Reaction has been toggled', 'fluent-community'),
461 695 'reactions_count' => $comment->reactions_count,
462 696 'liked' => $reactionState
463 697 ];
464 698 }
@@ -464,20 +698,63 @@
464 698 }
465 699
466 700 public function show(Request $request, $id)
467 701 {
468 - $comment = Comment::with([
469 - 'xprofile' => function ($q) {
470 - return $q->select(ProfileHelper::getXProfilePublicFields());
471 - }
472 - ])->findOrFail($id);
473 702
703 + $testComment = Comment::query()->findOrFail($id);
704 +
705 + $comment = Comment::byContentModerationAccessStatus($this->getUser(), $testComment->space)
706 + ->with([
707 + 'xprofile' => function ($q) {
708 + return $q->select(ProfileHelper::getXProfilePublicFields());
709 + }
710 + ])->findOrFail($id);
711 +
474 712 // Just to verify the permission
475 - $feed = Feed::withoutGlobalScopes()
713 + Feed::withoutGlobalScopes()
476 714 ->byUserAccess($this->getUserId())
477 715 ->findOrFail($comment->post_id);
478 716
479 - return [
717 + if ($request->get('context') == 'edit') {
718 + $meta = $comment->meta;
719 + unset($comment->meta);
720 + $images = Arr::get($meta, 'media_items', []);
721 + if ($images) {
722 + $comment->media_images = $images;
723 + } else {
724 + $preview = Arr::get($meta, 'media_preview', []);
725 + if ($preview) {
726 + $previewUrl = Arr::get($preview, 'image');
727 + $provider = Arr::get($preview, 'provider');
728 + if ($previewUrl && $provider == 'uploader') {
729 + $media = Media::where('media_url', $previewUrl)
730 + ->where('object_source', 'comment')
731 + ->where('sub_object_id', $comment->id)
732 + ->first();
733 + if ($media) {
734 + $comment->media_images = [
735 + [
736 + 'media_id' => $media->id,
737 + 'url' => $media->public_url,
738 + 'type' => $media->media_type,
739 + 'width' => Arr::get($media->settings, 'width'),
740 + 'height' => Arr::get($media->settings, 'height'),
741 + 'provider' => Arr::get($media->settings, 'provider', 'uploader')
742 + ]
743 + ];
744 + }
745 + } else {
746 + $comment->meta = [
747 + 'media_preview' => $preview
748 + ];
749 + }
750 + }
751 + }
752 + }
753 +
754 + $data = [
480 755 'comment' => $comment
481 756 ];
757 +
758 + return apply_filters('fluent_community/comment_api_response', $data, $request->all());
482 759 }
483 760 }