# fluent-community/trunk/app/Http/Controllers/FeedsController.php

FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS &amp; Online Courses, version trunk. 1,522 lines.

- Page: https://pluginprobe.com/plugins/fluent-community/trunk/code/app/Http/Controllers/FeedsController.php
- Raw: https://pluginprobe.com/plugins/fluent-community/trunk/raw/app/Http/Controllers/FeedsController.php
- Modified: 2026-09-14T14:31:46+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/fluent-community/trunk/code/app/Http/Controllers/FeedsController.php#L10-L20`.

```php
<?php

namespace FluentCommunity\App\Http\Controllers;

use FluentCommunity\App\Functions\Utility;
use FluentCommunity\App\Models\Media;
use FluentCommunity\App\Models\Notification;
use FluentCommunity\App\Models\NotificationSubscriber;
use FluentCommunity\App\Models\Space;
use FluentCommunity\App\Models\User;
use FluentCommunity\App\Services\CustomSanitizer;
use FluentCommunity\App\Services\FeedsHelper;
use FluentCommunity\App\Services\Helper;
use FluentCommunity\App\Services\Libs\FileSystem;
use FluentCommunity\App\Services\UploadHelper;
use FluentCommunity\App\Services\RemoteUrlParser;
use FluentCommunity\Framework\Http\Request\Request;
use FluentCommunity\App\Models\Feed;
use FluentCommunity\App\Models\BaseSpace;
use FluentCommunity\App\Models\XProfile;
use FluentCommunity\Framework\Support\Arr;
use FluentCommunity\Modules\PushNotification\PushNotificationModule;

class FeedsController extends Controller
{
    public function get(Request $request)
    {
        $start = microtime(true);
        $space = null;
        $bySpace = $request->get('space');
        $userId = $request->getSafe('user_id', 'intval', '');
        $selectedTopic = $request->getSafe('topic_slug', 'sanitize_text_field', '');
        $search = $request->getSafe('search', 'sanitize_text_field', '');
        if ($bySpace) {
            // just for validation
            $space = BaseSpace::where('slug', $bySpace)->first();
            if (!$space) {
                return $this->sendError(['message' => __('Invalid space slug', 'fluent-community')]);
            }
        }

        $currentUserModel = $this->getUser();
        $currentUserId = get_current_user_id();

        $isOwnProfile = $userId && (int)$userId === (int)$currentUserId;

        $filterableStatuses = apply_filters('fluent_community/feed/filterable_statuses', []);

        $statusFilter = $request->getSafe('status', 'sanitize_text_field', '');

        $applyStatusFilter = $statusFilter
            && in_array($statusFilter, $filterableStatuses, true)
            && (Helper::isModerator() || $isOwnProfile);

        $maxPerPage = (int) apply_filters('fluent_community/max_per_page', 100) ?: 100;

        $queryArgs = [
            'selected_topic' => $selectedTopic,
            'per_page'       => min($maxPerPage, max(1, (int)$request->get('per_page', 10))),
            'page'           => max(1, (int)$request->get('page', 1)),
            'search'         => $search,
        ];

        $feedsQuery = Feed::select(Feed::$publicColumns)
            ->with(Feed::withPublicRelations($currentUserModel, $space))
            ->searchBy($search, (array)$request->get('search_in', ['post_content']))
            ->byTopicSlug($selectedTopic)
            ->customOrderBy($request->getSafe('order_by_type'));

        if ($applyStatusFilter) {
            $feedsQuery->byStatus($statusFilter);
        } else {
            $feedsQuery->byContentModerationAccessStatus($currentUserModel, $space);
        }

        $stickyFeed = null;

        $disableSticky = $request->get('disable_sticky', '') == 'yes' || !!$search || !!$selectedTopic;

        if ($bySpace) {
            $feedsQuery = $feedsQuery->filterBySpaceSlug($bySpace);
            $queryArgs['space_slug'] = $bySpace;
        }

        if ($bySpace && !$disableSticky) {
            $feedsQuery = $feedsQuery->where('is_sticky', 0);
            if ($queryArgs['page'] === 1) {
                $stickyFeed = Feed::where('space_id', $space->id)
                    ->where('is_sticky', 1)
                    ->byUserAccess($currentUserId)
                    ->byContentModerationAccessStatus($currentUserModel, $space)
                    ->with(Feed::withPublicRelations($this->getUser(), $space))
                    ->first();
            }
        }

        if ($userId) {
            $feedsQuery = $feedsQuery->where('user_id', $userId);

            if (!Helper::isModerator()) {
                $feedsQuery = $feedsQuery->whereHas('xprofile', function ($q) {
                    $q->where('status', 'active');
                });
            }

            if ($userId != $currentUserId) {
                $feedsQuery = $feedsQuery->byUserAccess($currentUserId);
            }

            $queryArgs['user_id'] = $userId;
        } else {
            $feedsQuery->byUserAccess($currentUserId)->whereHas('xprofile', function ($q) {
                $q->where('status', 'active');
            });
        }

        $queryArgs = array_filter($queryArgs);
        $queryArgs['is_main_query'] = empty($queryArgs['space_slug']) && empty($queryArgs['user_id']) && empty($queryArgs['search']);

        do_action_ref_array('fluent_community/feeds_query', [&$feedsQuery, $request->all(), $queryArgs]);

        $feedsQuery->limit($queryArgs['per_page'])->offset(($queryArgs['page'] - 1) * $queryArgs['per_page']);
        $feeds = $feedsQuery->get();

        // add $stickyFeed to the first page
        if ($stickyFeed) {
            $stickyFeed = FeedsHelper::transformFeed($stickyFeed);
        }

        $feeds = FeedsHelper::transformFeedsCollection($feeds);

        $currentCount = $feeds->count();
        $to = ($queryArgs['page'] - 1) * $queryArgs['per_page'] + $currentCount;

        $hasMore = $currentCount == $queryArgs['per_page'];

        $data = [
            'feeds'  => [
                'data'         => $feeds,
                'current_page' => $queryArgs['page'],
                'per_page'     => $queryArgs['per_page'],
                'from'         => $currentCount ? ($queryArgs['page'] - 1) * $queryArgs['per_page'] + 1 : 0,
                'to'           => $to,
                'has_more'     => $hasMore,
                'total'        => $currentCount == $queryArgs['per_page'] ? $to + $currentCount : $to
            ],
            'sticky' => $stickyFeed
        ];

        $isMainFeed = $queryArgs['page'] === 1 && !$search && !$userId;
        if ($isMainFeed && $currentUserId) {
            $data['last_fetched_timestamp'] = current_time('timestamp');
        }

        $data['execution_time'] = microtime(true) - $start;

        $data = apply_filters('fluent_community/feeds_api_response', $data, $request->all());

        return $data;
    }

    public function getFeedBySlug(Request $request, $feed_slug)
    {
        $start = microtime(true);
        if ($request->get('context') == 'edit') {
            $feed = Feed::where('slug', $feed_slug)->first();

            if (!$feed || !$feed->hasEditAccess(get_current_user_id())) {
                return $this->sendError([
                    'message' => __('You do not have permission to edit this feed', 'fluent-community')
                ]);
            }

            $data = [
                'feed' => FeedsHelper::transformForEdit($feed)
            ];

            return apply_filters('fluent_community/feed_api_response', $data, $request->all());
        }

        $feed = Feed::where('slug', $feed_slug)
            ->select(Feed::$publicColumns)
            ->with(Feed::withPublicRelations($this->getUser()))
            ->whereHas('xprofile', function ($q) {
                $q->where('status', 'active');
            })
            ->byUserAccess($this->getUserId())
            ->first();

        if (!$feed) {
            return $this->sendError([
                'message' => __('The feed could not be found', 'fluent-community')
            ], 404);
        }

        $viewableByLinkStatuses = FeedsHelper::getViewableByLinkStatuses();

        if (!in_array($feed->status, $viewableByLinkStatuses, true) && !$feed->hasEditAccess($this->getUserId())) {
            return $this->sendError([
                'message' => __('Sorry, you do not have permission to view this post', 'fluent-community')
            ], 404);
        }

        $feed = FeedsHelper::transformFeed($feed);

        return apply_filters('fluent_community/feed_api_response', [
            'feed'           => $feed,
            'execution_time' => microtime(true) - $start
        ], $request->all());

    }

    public function getFeedById(Request $request, $feedId)
    {
        $feed = Feed::findOrFail($feedId);
        return $this->getFeedBySlug($request, $feed->slug);
    }

    public function getBookmarks(Request $request)
    {
        $userId = $this->getUserId();

        $feedsQuery = Feed::where('status', 'published')
            ->select(Feed::$publicColumns)
            ->with(Feed::withPublicRelations($this->getUser()))
            ->byBookMarked($userId)
            ->byUserAccess($userId)
            ->byTopicSlug($request->getSafe('topic_slug'))
            ->customOrderBy($request->getSafe('order_by_type'))
            ->searchBy($request->getSafe('search'));

        if ($type = $request->get('type')) {
            $feedsQuery = $feedsQuery->where('type', $type);
        }

        $queryArgs = [
            'per_page' => (int)$request->get('per_page', 10),
            'page'     => (int)$request->get('page', 1)
        ];

        $feeds = $feedsQuery->orderBy('id', 'DESC')
            ->limit($queryArgs['per_page'])
            ->offset(($queryArgs['page'] - 1) * $queryArgs['per_page'])
            ->get();

        $currentCount = $feeds->count();
        $to = ($queryArgs['page'] - 1) * $queryArgs['per_page'] + $currentCount;

        $hasMore = $currentCount == $queryArgs['per_page'];

        $feeds = FeedsHelper::transformFeedsCollection($feeds);

        $data = [
            'feeds' => [
                'data'         => $feeds,
                'current_page' => $queryArgs['page'],
                'per_page'     => $queryArgs['per_page'],
                'from'         => $currentCount ? ($queryArgs['page'] - 1) * $queryArgs['per_page'] + 1 : 0,
                'to'           => $to,
                'has_more'     => $hasMore,
                'total'        => $currentCount == $queryArgs['per_page'] ? $to + $currentCount : $to
            ]
        ];

        if ($queryArgs['page'] === 1) {
            $lastItem = FeedsHelper::getLastFeedId();
            if ($lastItem) {
                $data['last_id'] = $lastItem;
            }
        }

        return apply_filters('fluent_community/bookmarks_api_response', $data, $request->all());
    }

    public function store(Request $request)
    {
        $user = $this->getUser(true);

        do_action('fluent_community/check_rate_limit/create_post', $user);

        $requestData = $request->all();

        $data = $this->sanitizeAndValidateData($requestData);
        $data['user_id'] = $user->ID;
        $data['status'] = 'published';

        $data['status'] = apply_filters('fluent_community/feed/save_status', $data['status'], $requestData, null);

        $feed = new Feed();
        $feed->user_id = $user->ID;
        $space = null;

        if ($spaceSlug = $request->get('space')) {
            $data['space_id'] = $this->validateAndSetSpace($spaceSlug, $user);
            if ($data['space_id']) {
                $space = Space::where('id', $data['space_id'])->first();
                if (!$space) {
                    return $this->sendError([
                        'message' => __('Please select a valid space to post in.', 'fluent-community')
                    ]);
                }
            }

            if ($space && Arr::get($space->settings, 'topic_required') == 'yes') {
                $topicIds = (array)$request->get('topic_ids', []);
                $spaceTopics = Utility::getTopicsBySpaceId($space->id);
                $spaceTopicsIds = [];

                foreach ($spaceTopics as $topic) {
                    $spaceTopicsIds[] = $topic['id'];
                }

                $validTopicIds = array_intersect($topicIds, $spaceTopicsIds);

                if (!$validTopicIds) {
                    return $this->sendError([
                        'message' => __('Please select at least one topic to post in this space.', 'fluent-community'),
                        'shakes'  => [
                            'topic_ids' => true
                        ]
                    ]);
                }
            }

        } else if (!Helper::hasGlobalPost()) {
            return $this->sendError([
                'message' => __('Please select a valid space to post in.', 'fluent-community')
            ]);
        }

        $spaceId = Arr::get($data, 'space_id');
        $message = Arr::get($data, 'message');

        $duplicateCheckMessage = $message;

        $mentions = FeedsHelper::getMentions($data['message'], Arr::get($data, 'space_id'), true);
        if ($mentions) {
            $data['message'] = $message;
            $message = $mentions['text'];
        }

        [$message, $inlineMedias] = FeedsHelper::replaceImageUrlsWithRealMediaArchive($message);

        // replace new line with br
        $data['message_rendered'] = wp_kses_post(FeedsHelper::mdToHtml($message));

        $requestData['is_admin'] = $user->hasPermissionOrInCurrentSpace('community_moderator', $space);

        [$data, $mediaItems] = FeedsHelper::processFeedMetaData($data, $requestData);

        if ($inlineMedias) {
            $mediaItems = array_merge($mediaItems, $inlineMedias);
        }

        if (Arr::get($requestData, 'send_announcement_email') == 'yes' && $requestData['is_admin']) {
            $data['meta']['send_announcement_email'] = 'yes';
        } else if (isset($data['meta']['send_announcement_email'])) {
            $data['meta']['send_announcement_email'] = 'no';
        }

        if ($mentions) {
            $data['meta']['mentioned_user_ids'] = Arr::get($mentions, 'user_ids', []);
        }

        $data = apply_filters('fluent_community/feed/new_feed_data', $data, $requestData);

        $formContentType = (string)Arr::get($requestData, 'content_type', '');

        if ($formContentType) {
            $data = apply_filters('fluent_community/feed/new_feed_data_type_' . $formContentType, $data, $requestData);
        }

        if (is_wp_error($data)) {
            return $this->sendError([
                'message' => $data->get_error_message(),
                'errors'  => $data->get_error_data()
            ]);
        }

        $feed->fill($data);

        // Serialize a member's concurrent submissions by locking their profile row,
        // so parallel matching requests cannot pass the duplicate check and both insert.
        $isDuplicate = Helper::dbTransaction(function () use ($feed, $user, $spaceId, $duplicateCheckMessage) {
            XProfile::where('user_id', $user->ID)->lockForUpdate()->first();

            if ($duplicate = $this->checkForDuplicatePost($user->ID, $duplicateCheckMessage, $spaceId)) {
                return $duplicate;
            }

            $feed->save();

            return null;
        });

        if ($isDuplicate) {
            return $isDuplicate;
        }

        $feed = Feed::find($feed->id); // just renewing the feed

        if ($mentions) {
            do_action('fluent_community/feed_mentioned', $feed, Arr::get($mentions, 'users'));
        }

        if ($formContentType) {
            do_action('fluent_community/feed/just_created_type_' . $formContentType, $feed, $requestData);
        }

        if ($mediaItems) {
            $this->saveMediaItems($feed, $mediaItems);
        }

        $feed->load(['xprofile', 'comments.xprofile']);
        if ($feed->space_id) {
            $feed->load(['space']);
            $topicIds = (array)$request->get('topic_ids', []);
            // take only max topics per post
            if ($topicIds) {
                $topicsConfig = Helper::getTopicsConfig();
                $topicIds = array_slice($topicIds, 0, $topicsConfig['max_topics_per_post']);
                $feed->attachTopics($topicIds, false);
                $feed->load(['terms']);
            }
        }


        if ($feed->status == 'scheduled') {
            do_action('fluent_community/feed/scheduled', $feed);
            /* translators: %s: The scheduled date and time for the post */
            $message = sprintf(__('Your post has been scheduled for %s', 'fluent-community'), $feed->scheduled_at);
            return [
                'feed'                   => FeedsHelper::transformFeed($feed),
                'scheduled_at'           => $feed->scheduled_at,
                'message'                => $message,
                'last_fetched_timestamp' => current_time('timestamp')
            ];
        }

        if (!in_array($feed->status, ['published', 'unlisted'])) {
            do_action('fluent_community/feed/new_feed_' . $feed->status, $feed);
            /* translators: %s: The status of the post */
            $message = sprintf(__('Your post has been marked as %s', 'fluent-community'), $feed->status);
            return apply_filters('fluent_community/feed/new_feed_response', [
                'feed'                   => FeedsHelper::transformFeed($feed),
                'message'                => $message,
                'last_fetched_timestamp' => current_time('timestamp')
            ], $feed, $request->all());
        }

        do_action('fluent_community/feed/created', $feed);

        if ($feed->space_id) {
            do_action('fluent_community/space_feed/created', $feed);
        } else {
            do_action('fluent_community/profile_feed/created', $feed);
        }

        $message = __('Your post has been published', 'fluent-community');

        return apply_filters('fluent_community/feed/new_feed_response', [
            'feed'                   => FeedsHelper::transformFeed($feed),
            'message'                => $message,
            'last_fetched_timestamp' => current_time('timestamp')
        ], $feed, $request->all());
    }

    public function update(Request $request, $feedId)
    {
        $requestData = $request->all();
        $data = $this->sanitizeAndValidateData($requestData);
        $user = $this->getUser(true);
        $existingFeed = Feed::findOrFail($feedId);
        /** @var Feed $existingFeed */

        $editableStatuses = ['published', 'unlisted', 'scheduled', 'pending'];

        if (!in_array($existingFeed->status, $editableStatuses)) {
            return $this->sendError([
                'message' => __('Sorry, this post is not in an editable state.', 'fluent-community')
            ]);
        }

        $user->canEditFeed($existingFeed, true);

        // Must resolve before processFeedMetaData() reads it.
        $isModerator = $user->hasPermissionOrInCurrentSpace('community_moderator', $existingFeed->space);
        $requestData['is_admin'] = $isModerator;

        if ($surveyOptionError = FeedsHelper::getSurveyOptionsUpdateError(
            Arr::get($existingFeed->meta, 'survey_config.options', []),
            Arr::get($requestData, 'survey', [])
        )) {
            return $this->sendError([
                'message' => $surveyOptionError
            ]);
        }

        if ($isModerator && ($status = Arr::get($requestData, 'status'))) {
            if (in_array($status, $editableStatuses, true)) {
                $fallbackStatus = $status === 'unlisted' ? $existingFeed->status : $status;
                $data['status'] = apply_filters('fluent_community/feed/save_status', $fallbackStatus, $requestData, $existingFeed);
            }
        }

        $message = $data['message'];
        $mentions = FeedsHelper::getMentions($data['message'], Arr::get($data, 'space_id'));
        if ($mentions) {
            $data['message'] = $message;
            $message = $mentions['text'];
        }

        [$message, $inlineMedias] = FeedsHelper::replaceImageUrlsWithRealMediaArchive($message, $existingFeed);

        // replace new line with br
        $data['message_rendered'] = wp_kses_post(FeedsHelper::mdToHtml($message));

        [$data, $mediaItems] = FeedsHelper::processFeedMetaData($data, $requestData, $existingFeed);

        if($inlineMedias) {
            $mediaItems = array_merge($mediaItems, $inlineMedias);
        }

        if (isset($existingFeed->meta['comments_disabled'])) {
            $data['meta']['comments_disabled'] = $existingFeed->meta['comments_disabled'];
        }

        if (Arr::get($requestData, 'send_announcement_email') == 'yes' && $requestData['is_admin']) {
            $data['meta']['send_announcement_email'] = 'yes';
        } else if (Arr::get($existingFeed->meta, 'send_announcement_email')) {
            $data['meta']['send_announcement_email'] = Arr::get($existingFeed->meta, 'send_announcement_email');
        }

        $data = apply_filters('fluent_community/feed/update_feed_data', $data, $requestData);

        if (is_wp_error($data)) {
            return $this->sendError([
                'message' => $data->get_error_message(),
                'errors'  => $data->get_error_data()
            ]);
        }

        $newContentType = Arr::get($requestData, 'content_type', '');
        $existingContentType = $existingFeed->content_type;

        if (($newContentType === 'document' && empty($requestData['document_ids'])) || ($newContentType === '' && $existingContentType === 'document' && empty($requestData['survey']))) {
            $newContentType = $data['content_type'] = 'text';
        }

        if ($newContentType != $existingContentType) {
            // Content Type Changed
            do_action('fluent_community/feed/updating_content_type_old_' . $existingContentType, $existingFeed, $newContentType, $requestData);
        }

        if ($newContentType != 'text') {
            $data = apply_filters('fluent_community/feed/update_feed_data_type_' . $newContentType, $data, $requestData, $existingFeed);
            if (is_wp_error($data)) {
                return $this->sendError([
                    'message' => $data->get_error_message(),
                    'errors'  => $data->get_error_data()
                ]);
            }
        }

        if ($message != $existingFeed->message) {
            $data['meta']['last_edited'] = [
                'user_id' => $user->ID,
                'time'    => current_time('mysql')
            ];
        }

        $movingToProfile = false;

        if ($newSpaceId = $request->get('new_space_id')) {
            if (!Helper::isUserInSpace($existingFeed->user_id, $newSpaceId)) {
                return $this->sendError([
                    'message' => __('The author is not a member of the selected space', 'fluent-community')
                ]);
            }

            $newSpace = Space::findOrFail($newSpaceId);

            // check if the current user is admin
            if (!$user->hasPermissionOrInCurrentSpace('community_admin', $newSpace)) {
                return $this->sendError([
                    'message' => __('Sorry, you do not have permission to change the space for this post', 'fluent-community')
                ]);
            }

            $data['space_id'] = $newSpaceId;

            \FluentCommunity\App\Models\Activity::where('feed_id', $existingFeed->id)
                ->update(['space_id' => $newSpaceId]);
        } else if ($request->get('move_to_profile')) {
            if (!$user->hasPermissionOrInCurrentSpace('community_admin', $existingFeed->space)) {
                return $this->sendError([
                    'message' => __('Sorry, you do not have permission to move this post to a profile', 'fluent-community')
                ]);
            }

            $data['space_id'] = null;
            $movingToProfile = true;

            \FluentCommunity\App\Models\Activity::where('feed_id', $existingFeed->id)
                ->update(['space_id' => null]);
        }

        $data = apply_filters('fluent_community/feed/update_data', $data, $existingFeed);
        $existingFeed->fill($data);
        $dirty = $existingFeed->getDirty();

        $existingFeed->fill($data);
        $existingFeed->save();

        if ($message != $existingFeed->message) {
            $editHistory = $existingFeed->getCustomMeta('_edit_history', []);
            if (!$editHistory) {
                $editHistory = [];
            }

            $editHistory[] = array_filter([
                'user_id'      => $user->ID,
                'time'         => current_time('mysql'),
                'prev_message' => $existingFeed->message,
                'prev_title'   => $existingFeed->title
            ]);

            // get last 5 edit history
            $editHistory = array_slice($editHistory, -5);
            $existingFeed->updateCustomMeta('_edit_history', $editHistory);
        }

        $mediaItemIds = [];
        foreach ($mediaItems as $mediaItem) {
            $mediaItemIds[] = $mediaItem->id;
        }

        if (Arr::has($requestData, 'media_images')) {
            $deactivateQuery = Media::where('object_source', 'feed')
                ->where('feed_id', $existingFeed->id)
                ->whereNotIn('id', $mediaItemIds);

            if (empty(Arr::get($requestData, 'media_images'))) {
                $deactivateQuery->where('media_type', '!=', 'fluent_player');
            }

            $deactivateQuery->update(['is_active' => 0]);
        }

        if ($mediaItems) {
            $this->saveMediaItems($existingFeed, $mediaItems);
        }

        $existingFeed->load(['xprofile', 'comments.xprofile']);

        if ($existingFeed->space_id) {
            $existingFeed->load(['space']);
            $space = $existingFeed->space;
            $topicIds = (array)Arr::get($requestData, 'topic_ids', []);
            $topicsConfig = Helper::getTopicsConfig();
            // take only max topics per post
            if ($topicIds) {
                $topicIds = array_slice($topicIds, 0, $topicsConfig['max_topics_per_post']);
                $existingFeed->attachTopics($topicIds, true);
            } else {
                if ($space && Arr::get($space->settings, 'topic_required') != 'yes') {
                    $existingFeed->terms()->where('taxonomy_name', 'post_topic')->detach();
                }
            }
        } else if ($movingToProfile) {
            // Topics are space-scoped; a post moved to the profile must not keep them.
            $existingFeed->terms()->where('taxonomy_name', 'post_topic')->detach();
        }

        if ($dirty) {
            do_action('fluent_community/feed/updated', $existingFeed, $dirty);
            if ($existingFeed->space_id) {
                do_action('fluent_community/space_feed/updated', $existingFeed);
            }
        }

        $data = [
            'feed'    => FeedsHelper::transformFeed($existingFeed),
            'message' => __('Your post has been updated', 'fluent-community')
        ];

        return apply_filters('fluent_community/feed/update_feed_response', $data, $request->all());
    }

    public function patchFeed(Request $request, $feedId)
    {
        $feed = Feed::findOrFail($feedId);
        $user = $this->getUser(true);

        $isAuthor = $feed->user_id == $user->ID;
        $isMod = $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
        $isAdmin = $user->hasPermissionOrInCurrentSpace('community_admin', $feed->space);

        if (!$isMod && !$isAuthor && !$isAdmin) {
            return $this->sendError([
                'message' => __('You do not have permission to perform this action', 'fluent-community')
            ]);
        }

        $allData = $request->all();
        $validKeys = ['is_sticky', 'priority', 'comments_disabled'];

        if (!$isMod) {
            $validKeys = ['comments_disabled'];
        }

        $data = Arr::only($allData, $validKeys);

        $data = array_map('intval', $data);

        // List/unlist toggle — community-moderator only, routed through the shared save_status filter.
        if (Helper::isModerator($user)
            && ($reqStatus = Arr::get($allData, 'status'))
            && in_array($reqStatus, ['published', 'unlisted'], true)
            && in_array($feed->status, ['published', 'unlisted'], true)
        ) {
            $fallbackStatus = $reqStatus === 'unlisted' ? $feed->status : $reqStatus;
            $data['status'] = apply_filters('fluent_community/feed/save_status', $fallbackStatus, $allData, $feed);
        }

        if (isset($data['is_sticky'])) {
            $data['is_sticky'] = $data['is_sticky'] ? 1 : 0;
            if ($data['is_sticky'] && $feed->space_id) {
                // toBase() keeps the type scope but skips the Orm update()'s updated_at stamp, which would bump the post being un-stuck.
                Feed::where('space_id', $feed->space_id)
                    ->where('is_sticky', 1)
                    ->toBase()
                    ->update(['is_sticky' => 0]);
            }
        }

        if (isset($data['comments_disabled'])) {
            $meta = $feed->meta;
            $meta['comments_disabled'] = $data['comments_disabled'] ? 'yes' : 'no';
            $data['meta'] = $meta;
        }

        if ($data) {
            $feed->fill($data);
            $dirty = $feed->getDirty();
            if ($dirty) {
                // Only a real list/unlist transition is activity, so read $dirty, not the request.
                if (!array_key_exists('status', $dirty)) {
                    $feed->timestamps = false;
                }

                $feed->save();
                do_action('fluent_community/feed/updated', $feed, $dirty);
            }
        }

        return apply_filters('fluent_community/feed/patch_feed_response', [
            'feed'    => $feed,
            'message' => __('Feed updated', 'fluent-community')
        ], $feed, $request->all());
    }

    public function getWelcomeBanner(Request $request)
    {
        $scope = get_current_user_id() ? 'login' : 'logout';

        $data = [
            'welcome_banner' => Helper::getWelcomeBanner($scope)
        ];

        return apply_filters('fluent_community/welcome_banner_api_response', $data, $request->all());
    }

    public function getLinks(Request $request)
    {
        $scope = $request->getSafe('scope');

        if ($scope == 'view') {
            $data = [
                'links' => Helper::getEnabledFeedLinks()
            ];

            return apply_filters('fluent_community/feed_links_api_response', $data, $request->all());
        }

        $data = [
            'links' => Helper::getFeedLinks()
        ];

        return apply_filters('fluent_community/feed_links_api_response', $data, $request->all());
    }

    public function updateLinks(Request $request)
    {
        $links = $request->get('links', []);

        $links = array_map(function ($link) {
            return CustomSanitizer::santizeLinkItem($link);
        }, $links);

        Helper::updateFeedLinks($links);

        return [
            'message' => __('Links have been updated.', 'fluent-community'),
            'links'   => $links
        ];
    }

    private function saveMediaItems($feed, $mediaItems)
    {
        foreach ($mediaItems as $media) {
            $media->feed_id = $feed->id;
            $media->is_active = 1;
            $media->object_source = 'feed';
            $media->save();
        }
    }

    private function sanitizeAndValidateData($data)
    {
        $data['type'] = 'text';

        $this->validate($data, [
            'message' => 'required'
        ], [
            'message.required' => __('Message is required', 'fluent-community'),
        ]);

        return FeedsHelper::sanitizeAndValidateData($data);
    }

    private function checkForDuplicatePost($userId, $message, $spaceId = null)
    {
        if (apply_filters('fluent_community/disable_duplicate_post_check', false, $userId, $spaceId)) {
            return false;
        }

        $message = trim($message);

        $exist = Feed::where('user_id', $userId)
            ->where('message', $message)
            ->where('created_at', '>', gmdate('Y-m-d H:i:s', current_time('timestamp') - 7 * 24 * 60 * 60))
            ->when($spaceId, function ($query) use ($spaceId) {
                $query->where('space_id', $spaceId);
            })
            ->first();

        if ($exist) {
            return $this->sendError(['message' => __('No duplicate post please!', 'fluent-community')]);
        }

        return false;
    }

    private function validateAndSetSpace($spaceSlug, $user)
    {
        if ($spaceSlug == '__self__post__') {
            if (!Helper::hasGlobalPost()) {
                throw new \Exception(esc_html__('Please select a valid space to post in', 'fluent-community'));
            }

            return null;
        }

        $space = Space::where('slug', $spaceSlug)->first();

        if (!$space) {
            throw new \Exception(esc_html__('Please select a valid space to post in', 'fluent-community'));
        }

        $user->verifySpacePermission('can_create_post', $space);

        return $space->id;
    }

    public function deleteFeed(Request $request, $feed_id)
    {
        $feed = Feed::findOrFail($feed_id);

        $user = User::find(get_current_user_id());
        $user->canDeleteFeed($feed, true);
        do_action('fluent_community/feed/before_deleted', $feed);
        $feed->delete();

        do_action('fluent_community/feed/deleted', $feed_id);

        return [
            'message' => __('Feed has been deleted successfully', 'fluent-community')
        ];
    }

    public function deleteMediaPreview(Request $request, $feed_id)
    {
        $feed = Feed::findOrFail($feed_id);
        $user = User::find(get_current_user_id());
        $user->canDeleteFeed($feed, true);

        //do_action('fluent_community/feed/media_deleted', $feed->media);

        $meta = $feed->meta;
        $meta['media_preview'] = null;

        $feed->meta = $meta;
        $feed->save();

        return [
            'message' => __('Media preview image has been removed successfully.', 'fluent-community')
        ];
    }

    public function handleMediaUpload(Request $request)
    {
        if ($error = Helper::checkUploadSizeError()) {
            return $this->sendError($error, 413);
        }

        $user = $this->getUser(true);

        do_action('fluent_community/check_rate_limit/media_upload', $user);

        $allowedMimeTypesArray = apply_filters('fluent_community/support_attachment_types', [
            'image/jpeg',
            'image/pjpeg',
            'image/png',
            'image/gif',
            'image/webp',
            'image/heic',
        ]);

        $allowedTypes = implode(',', $allowedMimeTypesArray);

        // Extensions eligible for WebP conversion (from allowed MIME types, excluding webp)
        $convertibleExtensions = [];
        foreach ($allowedMimeTypesArray as $mime) {
            $element = explode('/', $mime);
            $ext = end($element);
            if ($ext === 'pjpeg') {
                $ext = 'jpeg';
            }
            if ($ext && $ext !== 'webp' && !in_array($ext, $convertibleExtensions)) {
                $convertibleExtensions[] = $ext;
            }
        }
        // jpg is a common alias for jpeg — add only if jpeg is allowed
        if (in_array('jpeg', $convertibleExtensions)) {
            $convertibleExtensions[] = 'jpg';
        }

        $maxFileUnit = apply_filters('fluent_community/media_upload_max_file_unit', 'MB');
        $maxFileSize = apply_filters('fluent_community/media_upload_max_file_size', 100);

        $allowedFileSize = $maxFileSize;
        if (strtoupper($maxFileUnit) == 'MB') {
            $allowedFileSize = $maxFileSize * 1024;
        } else if (strtoupper($maxFileUnit) == 'GB') {
            $allowedFileSize = $maxFileSize * 1024 * 1024;
        }

        $files = $this->validate($this->request->files(), [
            'file' => 'required|mimetypes:' . $allowedTypes . '|max:' . $allowedFileSize,
        ], [
            'file.required'  => __('No upload file was received. Please try again.', 'fluent-community'),
            'file.mimetypes' => __('The file must be an image type.', 'fluent-community'),
            /* translators: %$1s is replaced by the maximum allowed file size, %2$s is replaced by the file size unit (e.g. MB) */
            'file.max'       => sprintf(__('The file size must be less than %1$s%2$s.', 'fluent-community'), $maxFileSize, $maxFileUnit)
        ]);

        if (Arr::get($files, 'file.type') === 'image/heic'
            && (!extension_loaded('imagick') || !class_exists('Imagick') || !in_array('HEIC', \Imagick::queryFormats('HEIC')))
        ) {
            return $this->sendError([
                'message' => __('HEIC image format is not supported on this system.', 'fluent-community')
            ]);
        }

        add_filter('wp_handle_upload', [UploadHelper::class, 'fixImageOrientation']);
        $uploadedFiles = FileSystem::put($files);
        remove_filter('wp_handle_upload', [UploadHelper::class, 'fixImageOrientation']);

        $file = Arr::get($uploadedFiles, 0);

        if (is_wp_error($file)) {
            return $this->sendError([
                'message' => $file->get_error_message()
            ]);
        }

        // an empty request body reaches here with nothing uploaded; never build media data from it
        if (!is_array($file) || empty($file['url']) || empty($file['file']) || empty($file['type'])) {
            return $this->sendError([
                'message' => __('No upload file was received. Please try again.', 'fluent-community')
            ]);
        }

        $upload_dir = wp_upload_dir();

        $originalUrl = $file['url'];
        $orginalPath = $upload_dir['basedir'] . '/fluent-community/' . $file['file'];
        $originalFileType = $file['type'];
        $originalFileName = $file['file'];

        $willWebPConvert = $request->get('disable_convert') != 'yes';

        $willWebPConvert = apply_filters('fluent_community/convert_image_to_webp', $willWebPConvert, $file);
        $willResize = $request->get('resize');
        $maxWidth = $request->get('max_width');

        $willResize = apply_filters('fluent_community/media_upload_resize', $willResize, $file);

        if ($context = $request->get('context')) {
            $maxWidth = apply_filters('fluent_community/media_upload_max_width_' . $context, $maxWidth, $file);
        }

        if ($willResize && $maxWidth) {
            $upload_dir = wp_upload_dir();
            $fileUrl = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $file['url']);

            $editor = wp_get_image_editor($fileUrl);

            if (!is_wp_error($editor) && $editor->get_size()['width'] > $maxWidth) {
                // Current file extension
                $ext = pathinfo($file['url'], PATHINFO_EXTENSION);
                $willConvert = in_array($ext, $convertibleExtensions) && $willWebPConvert;

                if ($willConvert) {
                    $dottedExtensions = array_map(function ($ext) {
                        return '.' . $ext;
                    }, $convertibleExtensions);

                    $fileUrl = str_replace($dottedExtensions, '.webp', $fileUrl);
                    $file['file'] = str_replace($dottedExtensions, '.webp', $file['file']);
                    $file['url'] = str_replace($dottedExtensions, '.webp', $file['url']);
                    $file['type'] = 'image/webp';
                }

                // resize the image
                $editor->resize($maxWidth, null, false);
                $editor->set_quality(90);
                if ($willConvert) {
                    $result = $editor->save($fileUrl, 'image/webp');
                    if ($result['mime-type'] == 'image/webp') {
                        // remove original file now
                        wp_delete_file(str_replace('.webp', '.' . $ext, $fileUrl));
                    }
                    $file['is_converted'] = true;
                } else {
                    $result = $editor->save($fileUrl);
                }

                if ($result['mime-type'] != 'image/webp') {
                    $file['file'] = $originalFileName;
                    $file['url'] = $originalUrl;
                    $file['type'] = $result['mime-type'];
                }

                $file['meta'] = [
                    'width'  => $editor->get_size()['width'],
                    'height' => $editor->get_size()['height']
                ];
            }
            $file['path'] = $upload_dir['basedir'] . '/fluent-community/' . $file['file'];
        } else {
            $upload_dir = wp_upload_dir();
            $file['path'] = $upload_dir['basedir'] . '/fluent-community/' . $file['file'];
        }

        if ($willWebPConvert && empty($file['is_converted']) && !$request->get('skip_convert')) {
            $path = $file['path'];
            $extension = pathinfo($path, PATHINFO_EXTENSION);

            if ($extension != 'webp' && in_array($extension, $convertibleExtensions)) {
                // Let's convert to webp
                $editor = wp_get_image_editor($file['path']);
                if (!is_wp_error($editor)) {
                    $file['path'] = str_replace('.' . $extension, '.webp', $file['path']);
                    $file['url'] = str_replace('.' . $extension, '.webp', $file['url']);
                    $file['type'] = 'image/webp';
                    $result = $editor->save($file['path'], 'image/webp');

                    if ($result['mime-type'] != 'image/webp') {
                        $file['path'] = $orginalPath;
                        $file['url'] = $originalUrl;
                        $file['type'] = $result['mime-type'];
                    } else {
                        wp_delete_file($orginalPath);
                    }

                    $file['meta'] = [
                        'width'  => $editor->get_size()['width'],
                        'height' => $editor->get_size()['height']
                    ];
                }
            }
        }

        $mediaData = [
            'media_type' => $file['type'],
            'driver'     => 'local',
            'media_path' => $file['path'],
            'media_url'  => $file['url'],
            'settings'   => Arr::get($file, 'meta', [])
        ];

        $mediaData = apply_filters('fluent_community/media_upload_data', $mediaData, $file);

        if (is_wp_error($mediaData)) {
            return $this->sendError([
                'message' => $mediaData->get_error_message(),
                'errors'  => $mediaData->get_error_data()
            ]);
        }

        if (!$mediaData) {
            return $this->sendError([
                'message' => __('Error while uploading the media', 'fluent-community')
            ]);
        }

        // Let's create the media now
        $media = Media::create($mediaData);

        $mediaUrl = $media->public_url;

        $mediaUrl = add_query_arg([
            'media_key' => $media->media_key,
        ], $mediaUrl);

        return [
            'media' => [
                'url'       => $mediaUrl,
                'media_key' => $media->media_key,
                'type'      => $media->media_type,
                'width'     => Arr::get($media->settings, 'width'),
                'height'    => Arr::get($media->settings, 'height')
            ]
        ];
    }

    public function getTicker(Request $request)
    {
        $start = microtime(true);

        $userId = get_current_user_id();
        if (!$userId) {
            return [
                'timestamp'   => current_time('mysql', true),
                'has_changes' => false,
                'error'       => __('User not authenticated', 'fluent-community'),
                'feeds'       => []
            ];
        }

        do_action('fluent_community/track_activity');


        // Support both old and new format
        $since = $request->get('since');
        if (!$since) {
            $since = date('Y-m-d H:i:s', current_time('timestamp') - 60); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
        } else {
            $timestamp = strtotime($since);
            if (current_time('timestamp') - $timestamp > 300) {
                $since = date('Y-m-d H:i:s', current_time('timestamp') - 60); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
            }
        }

        $feedUpdates = [];
        $hasChanges = false;

        // Get feed updates if since timestamp provided
        if ($since) {
            // Get all updated/created feeds with full data (including relationships)
            $currentUserModel = Helper::getCurrentUser();
            $updatedFeeds = Feed::where('updated_at', '>', $since)
                ->where('status', 'published')
                ->byUserAccess($userId)
                ->with(Feed::withPublicRelations($currentUserModel, null))
                ->orderBy('updated_at', 'desc')
                ->limit(20) // Reduced limit since we're sending full data
                ->get();

            // Transform feeds to include all necessary data
            $transformedFeeds = FeedsHelper::transformFeedsCollection($updatedFeeds);

            foreach ($transformedFeeds as $feed) {
                $isNew = $feed->created_at >= $since;

                // Determine context (primary context)
                $context = 'global';
                if ($feed->space_id && $feed->space) {
                    $context = 'space-' . $feed->space->slug;
                }

                $feedUpdates[] = [
                    'id'         => $feed->id,
                    'updated_at' => $feed->updated_at,
                    'action'     => $isNew ? 'created' : 'updated',
                    'context'    => $context,
                    'user_id'    => $feed->user_id,
                    'feed_data'  => $feed // Include full feed data
                ];
            }

            $hasChanges = !empty($feedUpdates);
        }

        // Get notification count
        $notificationCount = NotificationSubscriber::unread()->where('user_id', $userId)->count();

        $newNotifications = $this->getToastNotifications($userId, $since, $notificationCount);

        $response = [
            'timestamp'      => current_time('mysql'),
            'has_changes'    => $hasChanges,
            'feeds'          => $feedUpdates,
            'notifications'  => [
                'unread_count' => $notificationCount,
                'new_count'    => count($newNotifications),
                'new_items'    => $newNotifications
            ],
            'spaces'         => [], // For future use
            'execution_time' => microtime(true) - $start
        ];

        return apply_filters('fluent_community/feed_ticker', $response, $request->all());
    }

    /**
     * Unread notifications that landed since the previous ticker check, shaped for the
     * in-app toast. Deliberately cheap:
     *
     *  - returns before touching the DB when the toast is filtered off or the user has
     *    nothing unread, so the steady state costs zero extra queries
     *  - the predicate is answered by the (user_id, is_read, object_type, updated_at)
     *    index added in NotificationUserMigrator, so this is a short range scan with
     *    no filesort - on a 177k-row table it examines a single row instead of the
     *    ~88k the single-column is_read index used to force
     *  - the cursor is the subscriber `updated_at`, not `created_at`: a re-notification
     *    ("X and 3 others reacted to your post") bumps the existing subscriber row in
     *    place instead of inserting a new one - see NotificationEventHandler
     *  - the xprofile eager load only fires when at least one row came back
     *
     * @param int    $userId
     * @param string $since MySQL datetime in site local time
     * @param int    $unreadCount
     * @return array
     */
    protected function getToastNotifications($userId, $since, $unreadCount)
    {
        if (!$unreadCount || !$since) {
            return [];
        }

        if (!apply_filters('fluent_community/enable_notification_toast', true, $userId)) {
            return [];
        }

        $limit = (int)apply_filters('fluent_community/notification_toast_limit', 3, $userId);

        if ($limit < 1) {
            return [];
        }

        $notifications = Notification::query()
            ->select([
                'fcom_notifications.id',
                'fcom_notifications.feed_id',
                'fcom_notifications.object_id',
                'fcom_notifications.src_user_id',
                'fcom_notifications.action',
                'fcom_notifications.content',
                'fcom_notifications.route',
                'fcom_notification_users.updated_at as notified_at'
            ])
            ->join('fcom_notification_users', 'fcom_notification_users.object_id', '=', 'fcom_notifications.id')
            ->where('fcom_notification_users.user_id', $userId)
            ->where('fcom_notification_users.is_read', 0)
            ->where('fcom_notification_users.object_type', 'notification')
            ->where('fcom_notification_users.updated_at', '>', $since)
            ->with(['xprofile' => function ($q) {
                return $q->select(['user_id', 'display_name', 'username', 'avatar']);
            }])
            ->orderBy('fcom_notification_users.updated_at', 'DESC')
            ->limit($limit)
            ->get();

        $commentIds = [];
        foreach ($notifications as $notification) {
            if (!in_array($notification->action, PushNotificationModule::PUSHED_ACTIONS, true)) {
                continue;
            }

            $commentIds[] = (int)$notification->object_id;
            $commentIds[] = (int)Arr::get((array)$notification->route, 'query.comment_id');
        }

        $pushedCommentIds = PushNotificationModule::getPushedCommentIds(
            $userId,
            array_values(array_filter(array_unique($commentIds)))
        );

        $items = [];

        foreach ($notifications as $notification) {
            $wasPushed = in_array($notification->action, PushNotificationModule::PUSHED_ACTIONS, true)
                && (in_array((int)$notification->object_id, $pushedCommentIds, true)
                    || in_array((int)Arr::get((array)$notification->route, 'query.comment_id'), $pushedCommentIds, true));

            // The push already told this member; a toast would say it twice.
            if ($wasPushed) {
                continue;
            }

            $xprofile = $notification->xprofile;

            $items[] = [
                'id'          => (int)$notification->id,
                'feed_id'     => $notification->feed_id ? (int)$notification->feed_id : null,
                'object_id'   => $notification->object_id ? (int)$notification->object_id : null,
                'action'      => $notification->action,
                'route'       => $notification->route,
                'text'        => $this->getToastText($notification->content),
                'notified_at' => $notification->notified_at,
                'avatar'      => $xprofile ? $xprofile->avatar : '',
                'name'        => $xprofile ? $xprofile->display_name : ''
            ];
        }

        return apply_filters('fluent_community/notification_toast_items', $items, $userId);
    }

    /**
     * Flatten stored notification HTML to a single line of plain text. The toast renders
     * this with v-text, so it must never carry markup back to the client.
     *
     * @param string $content
     * @return string
     */
    protected function getToastText($content)
    {
        if (!$content) {
            return '';
        }

        $text = wp_specialchars_decode(wp_strip_all_tags($content), ENT_QUOTES);
        $text = trim(preg_replace('/\s+/', ' ', $text));

        if (mb_strlen($text) > 140) {
            $text = mb_substr($text, 0, 140) . '...';
        }

        return $text;
    }

    public function batchFetch(Request $request)
    {
        $feedIds = $request->get('feed_ids', []);

        if (empty($feedIds) || !is_array($feedIds)) {
            return [
                'feeds' => [],
                'error' => __('No feed IDs provided', 'fluent-community')
            ];
        }

        $userId = get_current_user_id();

        // Limit to 20 feeds per batch to prevent abuse
        $feedIds = array_slice($feedIds, 0, 20);

        // Build query based on context
        $query = Feed::whereIn('id', $feedIds)
            ->where('status', 'published')
            ->byUserAccess($userId);

        $currentUserModel = $this->getUser();

        $feeds = $query
            ->with(Feed::withPublicRelations($currentUserModel))
            ->get();

        $feeds = FeedsHelper::transformFeedsCollection($feeds);

        return [
            'feeds' => $feeds,
            'count' => $feeds->count()
        ];
    }

    public function getTickerUpdates(Request $request)
    {
        $context = $request->get('context', 'global');
        $since = $request->get('since'); // ISO 8601 timestamp

        $userId = get_current_user_id();
        if (!$userId) {
            return [
                'updates'     => [],
                'timestamp'   => current_time('mysql', true),
                'has_changes' => false,
                'error'       => __('User not authenticated', 'fluent-community')
            ];
        }

        // Parse since timestamp
        try {
            $sinceDate = $since ? new \DateTime($since) : null;
        } catch (\Exception $e) {
            return [
                'updates'     => [],
                'timestamp'   => current_time('mysql', true),
                'has_changes' => false,
                'error'       => __('Invalid timestamp format', 'fluent-community')
            ];
        }

        // Build query based on context
        $query = Feed::query();

        if (strpos($context, 'space-') === 0) {
            $spaceSlug = str_replace('space-', '', $context);
            $space = Space::where('slug', $spaceSlug)->first();
            if ($space) {
                $query->where('space_id', $space->id);
            }
        } elseif (strpos($context, 'user-') === 0) {
            $targetUserId = str_replace('user-', '', $context);
            $query->where('user_id', $targetUserId);
        }

        // Apply access control
        $query->byUserAccess($userId);

        $updates = [];

        // Get updated feeds (updated_at changed)
        if ($sinceDate) {
            $updatedFeeds = (clone $query)
                ->where('updated_at', '>', $sinceDate->format('Y-m-d H:i:s'))
                ->where('status', 'published')
                ->select(['id', 'updated_at', 'created_at'])
                ->orderBy('updated_at', 'desc')
                ->limit(100)
                ->get();

            foreach ($updatedFeeds as $feed) {
                $isNew = $feed->created_at >= $sinceDate->format('Y-m-d H:i:s');

                $updates[] = [
                    'id'         => $feed->id,
                    'updated_at' => gmdate('c', strtotime($feed->updated_at)),
                    'action'     => $isNew ? 'created' : 'updated'
                ];
            }

            // Check for deleted feeds (status changed to deleted)
            $deletedFeeds = (clone $query)
                ->where('updated_at', '>', $sinceDate->format('Y-m-d H:i:s'))
                ->whereIn('status', ['deleted', 'draft'])
                ->select(['id', 'updated_at'])
                ->limit(50)
                ->get();

            foreach ($deletedFeeds as $feed) {
                $updates[] = [
                    'id'         => $feed->id,
                    'updated_at' => gmdate('c', strtotime($feed->updated_at)),
                    'action'     => 'deleted'
                ];
            }
        }

        return [
            'updates'     => $updates,
            'timestamp'   => current_time('mysql', true),
            'has_changes' => !empty($updates)
        ];
    }

    public function getOembed(Request $request)
    {
        $currentUser = $this->getUser(true);

        do_action('fluent_community/check_rate_limit/oembed', $currentUser);

        $url = $request->getSafe('url', 'sanitize_url');

        $metaData = RemoteUrlParser::parse($url);

        if ($metaData && !is_wp_error($metaData)) {
            $data = [
                'oembed' => $metaData
            ];
            return apply_filters('fluent_community/feed_oembed_api_response', $data, $request->all());
        }

        return $this->sendError([
            'message' => __('No oembed data found', 'fluent-community'),
            'url'     => $url
        ]);
    }

    public function markdownToHtml(Request $request)
    {
        $message = CustomSanitizer::unslashMarkdown($request->get('text', ''));

        $html = wp_kses_post(FeedsHelper::mdToHtml($message));

        $data = [
            'html' => $html
        ];

        $data['message_rendered'] = $html;

        if (in_array('meta', $request->get('with', [])) && $request->get('feed')) {
            [$data,] = FeedsHelper::processFeedMetaData($data, $request->get('feed'));
        }

        return $data;
    }
}

```
