feat: forum rich-text editor, emoji picker, mentions, discover nav, feed, uploads, profile
Forum: - TipTap WYSIWYG editor with full toolbar - @emoji-mart/react emoji picker (consistent with tweets) - @mention autocomplete with user search API - Fix PHP 8.4 parse errors in Blade templates - Fix thread data display (paginator items) - Align forum page widths to max-w-5xl Discover: - Extract shared _nav.blade.php partial - Add missing nav links to for-you page - Add Following link for authenticated users Feed/Posts: - Post model, controllers, policies, migrations - Feed page components (PostComposer, FeedCard, etc) - Post reactions, comments, saves, reports, sharing - Scheduled publishing support - Link preview controller Profile: - Profile page components (ProfileHero, ProfileTabs) - Profile API controller Uploads: - Upload wizard enhancements - Scheduled publish picker - Studio status bar and readiness checklist
This commit is contained in:
115
app/Services/Posts/PostService.php
Normal file
115
app/Services/Posts/PostService.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Posts;
|
||||
|
||||
use App\Models\Post;
|
||||
use App\Models\PostTarget;
|
||||
use App\Models\User;
|
||||
use App\Services\ContentSanitizer;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PostService
|
||||
{
|
||||
public function __construct(
|
||||
private PostCountersService $counters,
|
||||
private PostHashtagService $hashtags,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a new text post.
|
||||
*
|
||||
* @param User $user
|
||||
* @param string $visibility
|
||||
* @param string|null $body
|
||||
* @param array $targets [['type'=>'artwork','id'=>123], ...]
|
||||
* @return Post
|
||||
*/
|
||||
public function createPost(
|
||||
User $user,
|
||||
string $type,
|
||||
string $visibility,
|
||||
?string $body,
|
||||
array $targets = [],
|
||||
?array $linkPreview = null,
|
||||
?array $taggedUsers = null,
|
||||
?Carbon $publishAt = null,
|
||||
): Post {
|
||||
$sanitizedBody = $body ? ContentSanitizer::render($body) : null;
|
||||
|
||||
$status = ($publishAt && $publishAt->isFuture())
|
||||
? Post::STATUS_SCHEDULED
|
||||
: Post::STATUS_PUBLISHED;
|
||||
|
||||
$meta = [];
|
||||
if ($linkPreview && ! empty($linkPreview['url'])) {
|
||||
$meta['link_preview'] = array_intersect_key($linkPreview, array_flip(['url', 'title', 'description', 'image', 'site_name']));
|
||||
}
|
||||
if ($taggedUsers && count($taggedUsers) > 0) {
|
||||
$meta['tagged_users'] = array_map(
|
||||
fn ($u) => ['id' => (int) $u['id'], 'username' => (string) $u['username'], 'name' => (string) ($u['name'] ?? $u['username'])],
|
||||
array_slice($taggedUsers, 0, 10),
|
||||
);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($user, $type, $visibility, $sanitizedBody, $targets, $meta, $status, $publishAt) {
|
||||
$post = Post::create([
|
||||
'user_id' => $user->id,
|
||||
'type' => $type,
|
||||
'visibility' => $visibility,
|
||||
'body' => $sanitizedBody,
|
||||
'meta' => $meta ?: null,
|
||||
'status' => $status,
|
||||
'publish_at' => $publishAt,
|
||||
]);
|
||||
|
||||
foreach ($targets as $target) {
|
||||
PostTarget::create([
|
||||
'post_id' => $post->id,
|
||||
'target_type' => $target['type'],
|
||||
'target_id' => $target['id'],
|
||||
]);
|
||||
}
|
||||
|
||||
// Sync hashtags extracted from the body
|
||||
if ($sanitizedBody) {
|
||||
$this->hashtags->sync($post, $sanitizedBody);
|
||||
}
|
||||
|
||||
return $post;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update body/visibility of an existing post.
|
||||
*/
|
||||
public function updatePost(Post $post, ?string $body, ?string $visibility): Post
|
||||
{
|
||||
$updates = [];
|
||||
|
||||
if ($body !== null) {
|
||||
$updates['body'] = ContentSanitizer::render($body);
|
||||
}
|
||||
|
||||
if ($visibility !== null) {
|
||||
$updates['visibility'] = $visibility;
|
||||
}
|
||||
|
||||
$post->update($updates);
|
||||
|
||||
// Re-sync hashtags whenever body changes
|
||||
if (isset($updates['body'])) {
|
||||
$this->hashtags->sync($post, $updates['body']);
|
||||
}
|
||||
|
||||
return $post->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft-delete a post (cascades to targets/reactions/comments via DB).
|
||||
*/
|
||||
public function deletePost(Post $post): void
|
||||
{
|
||||
$post->delete();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user