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

756 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 // This is a normal User.
289 return $query->where(function ($q) use ($user) {
290 $q->where('status', 'published')
291 ->orWhere(function ($q) use ($user) {
292 $q->where('status', 'pending')
293 ->where('user_id', $user->ID);
294 });
295 });
296 }
297
298 public function scopeByBookMarked($query, $userId)
299 {
300 return $query->whereHas('reactions', function ($q) use ($userId) {
301 $q->where('user_id', $userId)
302 ->where('type', 'bookmark');
303 });
304 }
305
306 public function scopeByTopicSlug($query, $topicSlug)
307 {
308 if (!$topicSlug) {
309 return $query;
310 }
311
312 return $query->whereHas('terms', function ($q) use ($topicSlug) {
313 $topic = Term::where('taxonomy_name', 'post_topic')->where('slug', $topicSlug)->first();
314 if ($topic) {
315 $q->where('term_id', $topic->id);
316 }
317 });
318 }
319
320 public function scopeFilterBySpaceSlug($query, $space)
321 {
322 if (!$space) {
323 return $query;
324 }
325
326 $query->whereHas('space', function ($q) use ($space) {
327 $q->where('slug', $space);
328 });
329
330 return $query;
331 }
332
333 public function scopeByType($query, $type)
334 {
335 if (!$type) {
336 return $query;
337 }
338
339 $query->where('type', $type);
340
341 return $query;
342 }
343
344 public function scopeCustomOrderBy($query, $type)
345 {
346 $acceptedTypes = array_keys(Helper::getPostOrderOptions());
347
348 if (!in_array($type, $acceptedTypes) || $type == 'latest') {
349 return $query->orderBy('created_at', 'DESC');
350 }
351
352 if ($type == 'new_activity') {
353 return $query->orderBy('updated_at', 'DESC');
354 }
355
356 if ($type == 'oldest') {
357 return $query->orderBy('created_at', 'ASC');
358 }
359
360 if ($type == 'likes') {
361 return $query->orderBy('reactions_count', 'DESC');
362 }
363
364 if ($type == 'unanswered') {
365 return $query->where('comments_count', 0)
366 ->orderBy('created_at', 'DESC');
367 }
368
369 if ($type == 'alphabetical') {
370 return $query->orderBy('slug', 'ASC');
371 }
372
373 if ($type == 'popular') {
374 // sort by comments_count + reactions_count desc
375 return $query->orderByRaw('(reactions_count + (comments_count * 2)) DESC');
376 }
377
378 $query = apply_filters('fluent_community/custom_order_by', $query, $type);
379
380 return $query;
381 }
382
383 public function scopeByStatus($query, $status)
384 {
385 if (!$status) {
386 return $query->where('status', 'published');
387 }
388
389 $query->where('status', $status);
390
391 return $query;
392 }
393
394 public function scopeByFollowing($query, $userId = null)
395 {
396 $query->orderBy('updated_at', 'DESC');
397
398 if (!Helper::isFeatureEnabled('followers_module')) {
399 return $query;
400 }
401
402 if (!$userId) {
403 $userId = get_current_user_id();
404 }
405
406 if (!$userId) {
407 return $query;
408 }
409
410 return $query->whereHas('follows', function ($query) use ($userId) {
411 $query->where('follower_id', $userId);
412 });
413 }
414
415 public function scopeFilterByUserId($query, $userId)
416 {
417 if (!$userId) {
418 return $query;
419 }
420
421 $query->where('user_id', $userId);
422
423 return $query;
424 }
425
426 public function user()
427 {
428 return $this->belongsTo(User::class, 'user_id', 'ID');
429 }
430
431 public function xprofile()
432 {
433 return $this->belongsTo(XProfile::class, 'user_id', 'user_id');
434 }
435
436 public function space()
437 {
438 return $this->belongsTo(BaseSpace::class, 'space_id', 'id')
439 ->withoutGlobalScopes();
440 }
441
442 public function comments()
443 {
444 return $this->hasMany(Comment::class, 'post_id', 'id');
445 }
446
447 public function reactions()
448 {
449 return $this->hasMany(Reaction::class, 'object_id', 'id')
450 ->where('object_type', 'feed');
451 }
452
453 /**
454 * Eager-load closures for rendering a feed with its public relations
455 * (author, moderation-scoped comments, space, top reactions, topics).
456 */
457 public static function withPublicRelations($currentUserModel, $space = null)
458 {
459 return [
460 'xprofile' => function ($q) {
461 $q->select(ProfileHelper::getXProfilePublicFields());
462 },
463 'comments' => function ($q) use ($currentUserModel, $space) {
464 $q->byContentModerationAccessStatus($currentUserModel, $space)
465 ->with(['xprofile' => function ($q) {
466 $q->select(ProfileHelper::getXProfilePublicFields());
467 }])
468 ->whereHas('xprofile', function ($q) {
469 $q->where('status', 'active');
470 });
471 },
472 'space' => function ($q) {
473 $q->select(['id', 'title', 'slug', 'type', 'settings']);
474 },
475 'reactions' => function ($q) {
476 $q->with(['xprofile' => function ($query) {
477 $query->select(['user_id', 'avatar', 'display_name']);
478 }])
479 ->where('type', 'like')
480 ->limit(3);
481 },
482 'terms' => function ($q) {
483 $q->select(['title', 'slug'])
484 ->where('taxonomy_name', 'post_topic');
485 }
486 ];
487 }
488
489 // New Relationship: Follow records where this post's user_id is the followed_id
490 public function follows()
491 {
492 return $this->hasMany(Follow::class, 'followed_id', 'user_id');
493 }
494
495 public function surveyVotes()
496 {
497 return $this->hasMany(Reaction::class, 'object_id', 'id')
498 ->where('type', 'survey_vote');
499 }
500
501 public function terms()
502 {
503 return $this->belongsToMany(Term::class, 'fcom_term_feed', 'post_id', 'term_id');
504 }
505
506 public function hasUserReact($userId, $type = 'like')
507 {
508 if (!$userId) {
509 return false;
510 }
511
512 return Reaction::select([ 'id' ])
513 ->where('object_id', $this->id)
514 ->where('object_type', 'feed')
515 ->where('user_id', $userId)
516 ->where('type', $type)
517 ->exists();
518 }
519
520 public function hasEditAccess($userId)
521 {
522 if (!$userId) {
523 return false;
524 }
525
526 if ($this->user_id == $userId) {
527 return true;
528 }
529
530 $userModel = User::find($userId);
531
532 if (!$userModel) {
533 return false;
534 }
535
536 if ($this->space_id) {
537 return $userModel->hasSpacePermission('edit_any_feed', $this->space);
538 }
539
540 return $userModel->hasCommunityPermission('edit_any_feed');
541 }
542
543 public function getHumanExcerpt($length = 40)
544 {
545 $content = $this->title;
546 if (!$content) {
547 $content = $this->message;
548 }
549
550 return Helper::getHumanExcerpt($content, $length);
551 }
552
553 public function getPermalink()
554 {
555 $sectionPrefix = 'space';
556 $contentPrefix = 'post';
557 $isLesson = $this->type === 'course_lesson';
558
559 if ($isLesson) {
560 $sectionPrefix = 'course';
561 $contentPrefix = 'lessons';
562 }
563
564 $urlPath = $contentPrefix . '/' . $this->slug;
565
566 if ($this->space_id && $this->space) {
567 $urlPath = $sectionPrefix . '/' . $this->space->slug . '/' . $contentPrefix . '/' . $this->slug;
568 if ($isLesson) {
569 $urlPath .= '/view';
570 }
571 }
572
573 return Helper::baseUrl($urlPath);
574 }
575
576 public function getPermalinkAttribute()
577 {
578 return $this->getPermalink();
579 }
580
581 public function activities()
582 {
583 return $this->hasMany(Activity::class, 'feed_id', 'id');
584 }
585
586 public function notifications()
587 {
588 return $this->hasMany(Notification::class, 'feed_id', 'id');
589 }
590
591 public function media()
592 {
593 return $this->hasMany(Media::class, 'feed_id', 'id');
594 }
595
596 public function getSurveyCastsByUserId($userId = null)
597 {
598 if (!$userId || $this->content_type != 'survey') {
599 return [];
600 }
601
602 return Utility::getFromCache('survey_cast_' . $this->id . '_' . $userId, function () use ($userId) {
603 return Reaction::where('type', 'survey_vote')
604 ->where('user_id', $userId)
605 ->where('object_id', $this->id)
606 ->pluck('object_type')->toArray();
607 }, 86400);
608 }
609
610 public function updateCustomMeta($key, $value)
611 {
612 $exist = Meta::where('object_id', $this->id)
613 ->where('object_type', 'feed')
614 ->where('meta_key', $key)
615 ->first();
616
617 if ($exist) {
618 $exist->value = $value;
619 $exist->save();
620 } else {
621 Meta::create([
622 'object_id' => $this->id,
623 'object_type' => 'feed',
624 'meta_key' => $key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
625 'value' => $value,
626 ]);
627 }
628
629 return true;
630 }
631
632 public function getCustomMeta($key, $default = null)
633 {
634 $exist = Meta::where('object_id', $this->id)
635 ->where('object_type', 'feed')
636 ->where('meta_key', $key)
637 ->first();
638
639 if ($exist) {
640 return $exist->value;
641 }
642
643 return $default;
644 }
645
646 public function attachTopics($topicIds, $sync = false)
647 {
648 if ((!$topicIds && !$sync) || !$this->space_id) {
649 return $this;
650 }
651
652 // let's find the valid topics for this space
653 $spaceTopics = Utility::getTopicsBySpaceId($this->space_id);
654 $spaceTopicIds = array_map(function ($topic) {
655 return $topic['id'];
656 }, $spaceTopics);
657
658 $validTopicIds = array_filter($topicIds, function ($topicId) use ($spaceTopicIds) {
659 return in_array($topicId, $spaceTopicIds);
660 });
661
662 if ($sync) {
663 $this->terms()->sync($validTopicIds);
664 } else {
665 $this->terms()->attach($validTopicIds);
666 }
667
668 return $this;
669 }
670
671 public function getJsRoute()
672 {
673 if ($this->type == 'course_lesson') {
674 return [
675 'name' => 'view_lesson',
676 'params' => [
677 'course_slug' => $this->space ? $this->space->slug : 'uknown',
678 'lesson_slug' => $this->slug,
679 ],
680 ];
681 }
682
683 if ($this->space_id) {
684 $route = [
685 'name' => 'space_feed',
686 'params' => [
687 'space' => $this->space->slug,
688 'feed_slug' => $this->slug,
689 ],
690 ];
691 } else {
692 $route = [
693 'name' => 'single_feed',
694 'params' => [
695 'feed_slug' => $this->slug,
696 ],
697 ];
698 }
699
700 return $route;
701 }
702
703 public function recountStats()
704 {
705 $this->comments_count = Comment::where('post_id', $this->id)
706 ->where('type', 'comment')
707 ->count();
708
709 $this->reactions_count = Reaction::where('object_type', 'feed')->where('type', 'like')
710 ->where('object_id', $this->id)
711 ->count();
712
713 $this->save();
714 return $this;
715 }
716
717 public function isEnabledForEveryoneTag()
718 {
719 return ($this->meta['send_announcement_email'] ?? null) === 'yes' && Utility::hasEmailAnnouncementEnabled();
720 }
721
722 public function getFeedHtml($withPlaceholder = false, $buttonText = null)
723 {
724 $emailComposer = new \FluentCommunity\App\Services\Libs\EmailComposer();
725
726 if ($withPlaceholder) {
727 $postPermalink = '##feed_permalink##';
728 } else {
729 $postPermalink = $this->getPermalink();
730 }
731
732 $feedHtml = $this->message_rendered;
733 $feedHtml .= FeedsHelper::getMediaHtml($this->meta, $postPermalink);
734
735 $buttonText = $buttonText ? $buttonText : __('Join the conversation', 'fluent-community');
736
737 $emailComposer->addBlock('post_boxed_content', $feedHtml, [
738 'user' => $this->user,
739 'title' => $this->title,
740 'permalink' => $postPermalink,
741 'space_name' => $this->space ? $this->space->title : __('Community', 'fluent-community'),
742 'is_single' => true,
743 ]);
744
745 $emailComposer->addBlock('button', $buttonText, [
746 'link' => $postPermalink,
747 ]);
748
749 $emailComposer->setDefaultLogo();
750
751 $emailComposer->setDefaultFooter();
752
753 return $emailComposer->getHtml();
754 }
755 }
756