This commit is contained in:
2026-03-20 21:17:26 +01:00
parent 1a62fcb81d
commit 29c3ff8572
229 changed files with 13147 additions and 2577 deletions

View File

@@ -5,9 +5,11 @@ declare(strict_types=1);
namespace App\Services;
use App\Enums\ReactionType;
use App\Models\ActivityEvent;
use App\Models\Artwork;
use App\Models\ArtworkComment;
use App\Models\CommentReaction;
use App\Models\Story;
use App\Models\User;
use App\Models\UserMention;
use App\Services\ThumbnailPresenter;
@@ -74,13 +76,15 @@ final class CommunityActivityService
$commentModels = $this->fetchCommentModels($sourceLimit, repliesOnly: false);
$replyModels = $this->fetchCommentModels($sourceLimit, repliesOnly: true);
$reactionModels = $this->fetchReactionModels($sourceLimit);
$recordedActivities = $this->fetchRecordedActivities($sourceLimit);
$commentActivities = $commentModels->map(fn (ArtworkComment $comment) => $this->mapCommentActivity($comment, 'comment'));
$replyActivities = $replyModels->map(fn (ArtworkComment $comment) => $this->mapCommentActivity($comment, 'reply'));
$reactionActivities = $reactionModels->map(fn (CommentReaction $reaction) => $this->mapReactionActivity($reaction));
$mentionActivities = $this->fetchMentionActivities($sourceLimit);
$merged = $commentActivities
$merged = $recordedActivities
->concat($commentActivities)
->concat($replyActivities)
->concat($reactionActivities)
->concat($mentionActivities)
@@ -136,6 +140,89 @@ final class CommunityActivityService
];
}
private function fetchRecordedActivities(int $limit): Collection
{
$events = ActivityEvent::query()
->select(['id', 'actor_id', 'type', 'target_type', 'target_id', 'meta', 'created_at'])
->with([
'actor' => function ($query) {
$query
->select('id', 'name', 'username', 'role', 'is_active', 'created_at')
->with('profile:user_id,avatar_hash')
->withCount('artworks');
},
])
->whereHas('actor', fn ($query) => $query->where('is_active', true)->whereNull('deleted_at'))
->latest('created_at')
->limit($limit)
->get();
if ($events->isEmpty()) {
return collect();
}
$artworkIds = $events
->where('target_type', ActivityEvent::TARGET_ARTWORK)
->pluck('target_id')
->map(fn ($id) => (int) $id)
->unique()
->values()
->all();
$storyIds = $events
->where('target_type', ActivityEvent::TARGET_STORY)
->pluck('target_id')
->map(fn ($id) => (int) $id)
->unique()
->values()
->all();
$targetUserIds = $events
->where('target_type', ActivityEvent::TARGET_USER)
->pluck('target_id')
->map(fn ($id) => (int) $id)
->unique()
->values()
->all();
$artworks = empty($artworkIds)
? collect()
: Artwork::query()
->select('id', 'user_id', 'title', 'slug', 'hash', 'thumb_ext', 'published_at', 'deleted_at', 'is_public', 'is_approved')
->whereIn('id', $artworkIds)
->public()
->published()
->whereNull('deleted_at')
->get()
->keyBy('id');
$stories = empty($storyIds)
? collect()
: Story::query()
->select('id', 'creator_id', 'title', 'slug', 'cover_image', 'published_at', 'status')
->whereIn('id', $storyIds)
->published()
->get()
->keyBy('id');
$targetUsers = empty($targetUserIds)
? collect()
: User::query()
->select('id', 'name', 'username', 'role', 'is_active', 'created_at')
->with('profile:user_id,avatar_hash')
->withCount('artworks')
->whereIn('id', $targetUserIds)
->where('is_active', true)
->whereNull('deleted_at')
->get()
->keyBy('id');
return $events
->map(fn (ActivityEvent $event) => $this->mapRecordedActivity($event, $artworks, $stories, $targetUsers))
->filter()
->values();
}
private function fetchCommentModels(int $limit, bool $repliesOnly): Collection
{
return ArtworkComment::query()
@@ -262,6 +349,52 @@ final class CommunityActivityService
];
}
private function mapRecordedActivity(ActivityEvent $event, Collection $artworks, Collection $stories, Collection $targetUsers): ?array
{
if ($event->type === ActivityEvent::TYPE_COMMENT && $event->target_type === ActivityEvent::TARGET_ARTWORK) {
return null;
}
$artwork = $event->target_type === ActivityEvent::TARGET_ARTWORK
? $artworks->get((int) $event->target_id)
: null;
$story = $event->target_type === ActivityEvent::TARGET_STORY
? $stories->get((int) $event->target_id)
: null;
$targetUser = $event->target_type === ActivityEvent::TARGET_USER
? $targetUsers->get((int) $event->target_id)
: null;
if ($event->target_type === ActivityEvent::TARGET_ARTWORK && ! $artwork) {
return null;
}
if ($event->target_type === ActivityEvent::TARGET_STORY && ! $story) {
return null;
}
if ($event->target_type === ActivityEvent::TARGET_USER && ! $targetUser) {
return null;
}
$iso = $event->created_at?->toIso8601String();
return [
'id' => 'event:' . $event->id,
'type' => (string) $event->type,
'user' => $this->buildUserPayload($event->actor),
'artwork' => $this->buildArtworkPayload($artwork),
'story' => $this->buildStoryPayload($story),
'target_user' => $this->buildUserPayload($targetUser),
'meta' => is_array($event->meta) ? $event->meta : [],
'created_at' => $iso,
'time_ago' => $event->created_at?->diffForHumans(),
'sort_timestamp' => $iso,
];
}
private function fetchMentionActivities(int $limit): Collection
{
if (! Schema::hasTable('user_mentions')) {
@@ -384,6 +517,20 @@ final class CommunityActivityService
];
}
private function buildStoryPayload(?Story $story): ?array
{
if (! $story) {
return null;
}
return [
'id' => (int) $story->id,
'title' => html_entity_decode((string) ($story->title ?? 'Story'), ENT_QUOTES | ENT_HTML5, 'UTF-8'),
'url' => route('stories.show', ['slug' => $story->slug]),
'cover_url' => $story->cover_url,
];
}
private function buildCommentPayload(ArtworkComment $comment): array
{
$artwork = $this->buildArtworkPayload($comment->artwork);