PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.9.1
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.9.1
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
fluent-community / app / Models / Feed.php

Feed.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.9.1, at app/Models/Feed.php

755 lines 20.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCommunity\App\Models;
4
5 use FluentCommunity\App\Functions\Utility;
6 use FluentCommunity\App\Services\FeedsHelper;
7 use FluentCommunity\App\Services\Helper;
8 use FluentCommunity\App\Services\ProfileHelper;
9 use FluentCommunityPro\App\Models\Follow;
10
11 /**
12 * @property int $id
13 * @property int|null $user_id
14 * @property string|null $title
15 * @property string|null $slug
16 * @property string|null $message
17 * @property string|null $message_rendered
18 * @property string|null $type
19 * @property string|null $content_type
20 * @property int|null $space_id
21 * @property string|null $privacy
22 * @property string|null $status
23 * @property int|null $priority
24 * @property string|null $featured_image
25 * @property int|null $is_sticky
26 * @property string|null $expired_at
27 * @property string|null $scheduled_at
28 * @property int|null $comments_count
29 * @property int|null $reactions_count
30 * @property array $meta
31 * @property string|null $created_at
32 * @property string|null $updated_at
33 * @property-read User|null $user
34 * @property-read BaseSpace|null $space
35 * @property-read \FluentCommunity\Framework\Database\Orm\Collection $comments
36 * @property bool|null $has_user_react
37 * @property bool|null $bookmarked
38 * @property string|null $default_comment_sort_by
39 */
40 class Feed extends Model
41 {
42 protected $table = 'fcom_posts';
43
44 protected $guarded = [ 'id' ];
45
46 protected $casts = [
47 'comments_count' => 'int',
48 'reactions_count' => 'int',
49 'is_sticky' => 'int',
50 'priority' => 'int',
51 ];
52
53 protected $fillable = [
54 'user_id',
55 'title',
56 'slug',
57 'message',
58 'message_rendered',
59 'type',
60 'content_type',
61 'space_id',
62 'privacy',
63 'status',
64 'priority',
65 'featured_image',
66 'is_sticky',
67 'expired_at',
68 'scheduled_at',
69 'comments_count',
70 'reactions_count',
71 'meta',
72 'created_at',
73 'updated_at',
74 ];
75
76 protected $searchable = [
77 'message',
78 'title',
79 ];
80
81 public static $publicColumns = [
82 'id',
83 'slug',
84 'message_rendered',
85 'meta',
86 'title',
87 'featured_image',
88 'created_at',
89 'privacy',
90 'priority',
91 'type',
92 'content_type',
93 'slug',
94 'space_id',
95 'user_id',
96 'status',
97 'is_sticky',
98 'scheduled_at',
99 'comments_count',
100 'reactions_count',
101 ];
102
103 protected $appends = [
104 'permalink',
105 ];
106
107 public static $scopeType = 'text';
108
109 public static function boot()
110 {
111 parent::boot();
112
113 static::creating(function ($model) {
114 if (empty($model->user_id)) {
115 $model->user_id = get_current_user_id();
116 }
117 if (empty($model->slug)) {
118 $model->slug = self::generateNewSlug($model);
119 }
120
121 if (empty($model->meta)) {
122 $model->meta = self::getDefaultMeta();
123 }
124
125 if (empty($model->status)) {
126 $model->status = 'published';
127 }
128
129 });
130
131 static::addGlobalScope('type', function ($builder) {
132 $builder->where('type', self::$scopeType);
133 });
134
135 static::deleting(function ($feed) {
136 Media::where('feed_id', $feed->id)
137 ->update([
138 'is_active' => 0,
139 ]);
140 Reaction::where('object_id', $feed->id)
141 ->where(function ($query) {
142 $query->where('object_type', 'feed')
143 ->orWhere('type', 'survey_vote');
144 })
145 ->delete();
146 });
147
148 }
149
150 protected static function generateNewSlug($newModel)
151 {
152 if ($newModel->title) {
153 // Remove the emojis
154 $title = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $newModel->title);
155 // get the first 40 char from the title
156 $title = mb_substr($title, 0, 40, 'UTF-8');
157 } else {
158 // get the first 25 char from the message
159 $title = mb_substr($newModel->message, 0, 40, 'UTF-8');
160 }
161
162 $title = Helper::normalizeToAscii($title);
163 $title = remove_accents($title);
164
165 $title = strtolower($title);
166 // only allow alphanumeric, dash, and underscore
167 $title = trim(preg_replace('/[^a-z0-9-_]/', ' ', $title));
168
169 $title = sanitize_title($title, 'post-' . time());
170
171 // check if the slug is already exists
172 $slug = $title;
173
174 $count = 1;
175 while (self::where('slug', $slug)->exists()) {
176 if ($count == 5) {
177 $count = time();
178 }
179 $slug = $title . '-' . $count;
180 ++$count;
181 }
182
183 if (strlen($slug) <= 4) {
184 $slug = $slug . '-' . time();
185 }
186
187 return $slug;
188 }
189
190 protected static function getDefaultMeta()
191 {
192 return [
193 'preview_data' => null,
194 ];
195 }
196
197 public function setMetaAttribute($value)
198 {
199 $this->attributes['meta'] = maybe_serialize($value);
200 }
201
202 public function getMetaAttribute($value)
203 {
204 $meta = Utility::safeUnserialize($value);
205
206 if (!$meta) {
207 $meta = [];
208 }
209
210 return Helper::sanitizeStoredMediaPreview($meta);
211 }
212
213 public function scopeSearchBy($query, $search, $in = [])
214 {
215 if (!$search) {
216 return $query;
217 }
218
219 if (!$in || !is_array($in)) {
220 $in = [ 'post_content' ];
221 }
222
223 $fields = $this->searchable;
224 $query->where(function ($query) use ($fields, $search, $in) {
225 if (in_array('post_content', $in)) {
226 $query->where(array_shift($fields), 'LIKE', "%$search%");
227 foreach ($fields as $field) {
228 $query->orWhere($field, 'LIKE', "%$search%");
229 }
230
231 $query->orWhere(function ($q) use ($search) {
232 $q->where('content_type', 'document')
233 ->where('meta', 'LIKE', '%document_lists%title%' . $search . '%');
234 });
235
236 if ($in && in_array('post_comments', $in)) {
237 $query->orWhereHas('comments', function ($q) use ($search) {
238 return $q->where('message', 'LIKE', "%$search%");
239 });
240 }
241 } elseif ($in && in_array('post_comments', $in)) {
242 $query->whereHas('comments', function ($q) use ($search) {
243 return $q->where('message', 'LIKE', "%$search%");
244 });
245 }
246 });
247
248 return $query;
249 }
250
251 public function scopeByUserAccess($query, $userId)
252 {
253 if ($userId) {
254 return $query->where(function ($subQuery) use ($userId) {
255 $subQuery->where('user_id', $userId)->orWhereNull('space_id')
256 ->orWhereHas('space', function ($q) use ($userId) {
257 $spaceIds = get_user_meta($userId, '_fcom_space_ids', true);
258 if ($spaceIds) {
259 $q->whereIn('id', $spaceIds);
260 return $q;
261 }
262 return $q->where('privacy', 'public');
263 });
264 });
265 }
266
267 return $query->where(function ($q) {
268 $q->whereNull('space_id')
269 ->orWhereHas('space', function ($q) {
270 $q->where('privacy', 'public');
271 });
272 });
273 }
274
275 public function scopeByContentModerationAccessStatus($query, $user, $space = null)
276 {
277 if (!$user || !Helper::isFeatureEnabled('content_moderation')) {
278 return $query->where('status', 'published');
279 }
280
281 if (
282 $user->hasCommunityModeratorAccess() ||
283 ($space && $user->hasSpacePermission('edit_any_feed', $space))
284 ) {
285 return $query->whereIn('status', [ 'published', 'pending' ]);
286 }
287
288 return $query->where(function ($statusQuery) use ($user) {
289 $statusQuery->where('status', 'published')
290 ->orWhere(function ($subQuery) use ($user) {
291 $subQuery->where('status', 'pending')
292 ->where('user_id', $user->ID);
293 });
294 });
295 }
296
297 public function scopeByBookMarked($query, $userId)
298 {
299 return $query->whereHas('reactions', function ($q) use ($userId) {
300 $q->where('user_id', $userId)
301 ->where('type', 'bookmark');
302 });
303 }
304
305 public function scopeByTopicSlug($query, $topicSlug)
306 {
307 if (!$topicSlug) {
308 return $query;
309 }
310
311 return $query->whereHas('terms', function ($q) use ($topicSlug) {
312 $topic = Term::where('taxonomy_name', 'post_topic')->where('slug', $topicSlug)->first();
313 if ($topic) {
314 $q->where('term_id', $topic->id);
315 }
316 });
317 }
318
319 public function scopeFilterBySpaceSlug($query, $space)
320 {
321 if (!$space) {
322 return $query;
323 }
324
325 $query->whereHas('space', function ($q) use ($space) {
326 $q->where('slug', $space);
327 });
328
329 return $query;
330 }
331
332 public function scopeByType($query, $type)
333 {
334 if (!$type) {
335 return $query;
336 }
337
338 $query->where('type', $type);
339
340 return $query;
341 }
342
343 public function scopeCustomOrderBy($query, $type)
344 {
345 $acceptedTypes = array_keys(Helper::getPostOrderOptions());
346
347 if (!in_array($type, $acceptedTypes) || $type == 'latest') {
348 return $query->orderBy('created_at', 'DESC');
349 }
350
351 if ($type == 'new_activity') {
352 return $query->orderBy('updated_at', 'DESC');
353 }
354
355 if ($type == 'oldest') {
356 return $query->orderBy('created_at', 'ASC');
357 }
358
359 if ($type == 'likes') {
360 return $query->orderBy('reactions_count', 'DESC');
361 }
362
363 if ($type == 'unanswered') {
364 return $query->where('comments_count', 0)
365 ->orderBy('created_at', 'DESC');
366 }
367
368 if ($type == 'alphabetical') {
369 return $query->orderBy('slug', 'ASC');
370 }
371
372 if ($type == 'popular') {
373 // sort by comments_count + reactions_count desc
374 return $query->orderByRaw('(reactions_count + (comments_count * 2)) DESC');
375 }
376
377 $query = apply_filters('fluent_community/custom_order_by', $query, $type);
378
379 return $query;
380 }
381
382 public function scopeByStatus($query, $status)
383 {
384 if (!$status) {
385 return $query->where('status', 'published');
386 }
387
388 $query->where('status', $status);
389
390 return $query;
391 }
392
393 public function scopeByFollowing($query, $userId = null)
394 {
395 $query->orderBy('updated_at', 'DESC');
396
397 if (!Helper::isFeatureEnabled('followers_module')) {
398 return $query;
399 }
400
401 if (!$userId) {
402 $userId = get_current_user_id();
403 }
404
405 if (!$userId) {
406 return $query;
407 }
408
409 return $query->whereHas('follows', function ($query) use ($userId) {
410 $query->where('follower_id', $userId);
411 });
412 }
413
414 public function scopeFilterByUserId($query, $userId)
415 {
416 if (!$userId) {
417 return $query;
418 }
419
420 $query->where('user_id', $userId);
421
422 return $query;
423 }
424
425 public function user()
426 {
427 return $this->belongsTo(User::class, 'user_id', 'ID');
428 }
429
430 public function xprofile()
431 {
432 return $this->belongsTo(XProfile::class, 'user_id', 'user_id');
433 }
434
435 public function space()
436 {
437 return $this->belongsTo(BaseSpace::class, 'space_id', 'id')
438 ->withoutGlobalScopes();
439 }
440
441 public function comments()
442 {
443 return $this->hasMany(Comment::class, 'post_id', 'id');
444 }
445
446 public function reactions()
447 {
448 return $this->hasMany(Reaction::class, 'object_id', 'id')
449 ->where('object_type', 'feed');
450 }
451
452 /**
453 * Eager-load closures for rendering a feed with its public relations
454 * (author, moderation-scoped comments, space, top reactions, topics).
455 */
456 public static function withPublicRelations($currentUserModel, $space = null)
457 {
458 return [
459 'xprofile' => function ($q) {
460 $q->select(ProfileHelper::getXProfilePublicFields());
461 },
462 'comments' => function ($q) use ($currentUserModel, $space) {
463 $q->byContentModerationAccessStatus($currentUserModel, $space)
464 ->with(['xprofile' => function ($q) {
465 $q->select(ProfileHelper::getXProfilePublicFields());
466 }])
467 ->whereHas('xprofile', function ($q) {
468 $q->where('status', 'active');
469 });
470 },
471 'space' => function ($q) {
472 $q->select(['id', 'title', 'slug', 'type', 'settings']);
473 },
474 'reactions' => function ($q) {
475 $q->with(['xprofile' => function ($query) {
476 $query->select(['user_id', 'avatar', 'display_name']);
477 }])
478 ->where('type', 'like')
479 ->limit(3);
480 },
481 'terms' => function ($q) {
482 $q->select(['title', 'slug'])
483 ->where('taxonomy_name', 'post_topic');
484 }
485 ];
486 }
487
488 // New Relationship: Follow records where this post's user_id is the followed_id
489 public function follows()
490 {
491 return $this->hasMany(Follow::class, 'followed_id', 'user_id');
492 }
493
494 public function surveyVotes()
495 {
496 return $this->hasMany(Reaction::class, 'object_id', 'id')
497 ->where('type', 'survey_vote');
498 }
499
500 public function terms()
501 {
502 return $this->belongsToMany(Term::class, 'fcom_term_feed', 'post_id', 'term_id');
503 }
504
505 public function hasUserReact($userId, $type = 'like')
506 {
507 if (!$userId) {
508 return false;
509 }
510
511 return Reaction::select([ 'id' ])
512 ->where('object_id', $this->id)
513 ->where('object_type', 'feed')
514 ->where('user_id', $userId)
515 ->where('type', $type)
516 ->exists();
517 }
518
519 public function hasEditAccess($userId)
520 {
521 if (!$userId) {
522 return false;
523 }
524
525 if ($this->user_id == $userId) {
526 return true;
527 }
528
529 $userModel = User::find($userId);
530
531 if (!$userModel) {
532 return false;
533 }
534
535 if ($this->space_id) {
536 return $userModel->hasSpacePermission('edit_any_feed', $this->space);
537 }
538
539 return $userModel->hasCommunityPermission('edit_any_feed');
540 }
541
542 public function getHumanExcerpt($length = 40)
543 {
544 $content = $this->title;
545 if (!$content) {
546 $content = $this->message;
547 }
548
549 return Helper::getHumanExcerpt($content, $length);
550 }
551
552 public function getPermalink()
553 {
554 $sectionPrefix = 'space';
555 $contentPrefix = 'post';
556 $isLesson = $this->type === 'course_lesson';
557
558 if ($isLesson) {
559 $sectionPrefix = 'course';
560 $contentPrefix = 'lessons';
561 }
562
563 $urlPath = $contentPrefix . '/' . $this->slug;
564
565 if ($this->space_id && $this->space) {
566 $urlPath = $sectionPrefix . '/' . $this->space->slug . '/' . $contentPrefix . '/' . $this->slug;
567 if ($isLesson) {
568 $urlPath .= '/view';
569 }
570 }
571
572 return Helper::baseUrl($urlPath);
573 }
574
575 public function getPermalinkAttribute()
576 {
577 return $this->getPermalink();
578 }
579
580 public function activities()
581 {
582 return $this->hasMany(Activity::class, 'feed_id', 'id');
583 }
584
585 public function notifications()
586 {
587 return $this->hasMany(Notification::class, 'feed_id', 'id');
588 }
589
590 public function media()
591 {
592 return $this->hasMany(Media::class, 'feed_id', 'id');
593 }
594
595 public function getSurveyCastsByUserId($userId = null)
596 {
597 if (!$userId || $this->content_type != 'survey') {
598 return [];
599 }
600
601 return Utility::getFromCache('survey_cast_' . $this->id . '_' . $userId, function () use ($userId) {
602 return Reaction::where('type', 'survey_vote')
603 ->where('user_id', $userId)
604 ->where('object_id', $this->id)
605 ->pluck('object_type')->toArray();
606 }, 86400);
607 }
608
609 public function updateCustomMeta($key, $value)
610 {
611 $exist = Meta::where('object_id', $this->id)
612 ->where('object_type', 'feed')
613 ->where('meta_key', $key)
614 ->first();
615
616 if ($exist) {
617 $exist->value = $value;
618 $exist->save();
619 } else {
620 Meta::create([
621 'object_id' => $this->id,
622 'object_type' => 'feed',
623 'meta_key' => $key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
624 'value' => $value,
625 ]);
626 }
627
628 return true;
629 }
630
631 public function getCustomMeta($key, $default = null)
632 {
633 $exist = Meta::where('object_id', $this->id)
634 ->where('object_type', 'feed')
635 ->where('meta_key', $key)
636 ->first();
637
638 if ($exist) {
639 return $exist->value;
640 }
641
642 return $default;
643 }
644
645 public function attachTopics($topicIds, $sync = false)
646 {
647 if ((!$topicIds && !$sync) || !$this->space_id) {
648 return $this;
649 }
650
651 // let's find the valid topics for this space
652 $spaceTopics = Utility::getTopicsBySpaceId($this->space_id);
653 $spaceTopicIds = array_map(function ($topic) {
654 return $topic['id'];
655 }, $spaceTopics);
656
657 $validTopicIds = array_filter($topicIds, function ($topicId) use ($spaceTopicIds) {
658 return in_array($topicId, $spaceTopicIds);
659 });
660
661 if ($sync) {
662 $this->terms()->sync($validTopicIds);
663 } else {
664 $this->terms()->attach($validTopicIds);
665 }
666
667 return $this;
668 }
669
670 public function getJsRoute()
671 {
672 if ($this->type == 'course_lesson') {
673 return [
674 'name' => 'view_lesson',
675 'params' => [
676 'course_slug' => $this->space ? $this->space->slug : 'uknown',
677 'lesson_slug' => $this->slug,
678 ],
679 ];
680 }
681
682 if ($this->space_id) {
683 $route = [
684 'name' => 'space_feed',
685 'params' => [
686 'space' => $this->space->slug,
687 'feed_slug' => $this->slug,
688 ],
689 ];
690 } else {
691 $route = [
692 'name' => 'single_feed',
693 'params' => [
694 'feed_slug' => $this->slug,
695 ],
696 ];
697 }
698
699 return $route;
700 }
701
702 public function recountStats()
703 {
704 $this->comments_count = Comment::where('post_id', $this->id)
705 ->where('type', 'comment')
706 ->count();
707
708 $this->reactions_count = Reaction::where('object_type', 'feed')->where('type', 'like')
709 ->where('object_id', $this->id)
710 ->count();
711
712 $this->save();
713 return $this;
714 }
715
716 public function isEnabledForEveryoneTag()
717 {
718 return ($this->meta['send_announcement_email'] ?? null) === 'yes' && Utility::hasEmailAnnouncementEnabled();
719 }
720
721 public function getFeedHtml($withPlaceholder = false, $buttonText = null)
722 {
723 $emailComposer = new \FluentCommunity\App\Services\Libs\EmailComposer();
724
725 if ($withPlaceholder) {
726 $postPermalink = '##feed_permalink##';
727 } else {
728 $postPermalink = $this->getPermalink();
729 }
730
731 $feedHtml = $this->message_rendered;
732 $feedHtml .= FeedsHelper::getMediaHtml($this->meta, $postPermalink);
733
734 $buttonText = $buttonText ? $buttonText : __('Join the conversation', 'fluent-community');
735
736 $emailComposer->addBlock('post_boxed_content', $feedHtml, [
737 'user' => $this->user,
738 'title' => $this->title,
739 'permalink' => $postPermalink,
740 'space_name' => $this->space ? $this->space->title : __('Community', 'fluent-community'),
741 'is_single' => true,
742 ]);
743
744 $emailComposer->addBlock('button', $buttonText, [
745 'link' => $postPermalink,
746 ]);
747
748 $emailComposer->setDefaultLogo();
749
750 $emailComposer->setDefaultFooter();
751
752 return $emailComposer->getHtml();
753 }
754 }
755