354 lines
14 KiB
PHP
354 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Studio;
|
|
|
|
use App\Models\CollectionComment;
|
|
use App\Models\NovaCardComment;
|
|
use App\Models\StoryComment;
|
|
use App\Models\User;
|
|
use App\Services\NotificationService;
|
|
use App\Support\AvatarUrl;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
final class CreatorStudioActivityService
|
|
{
|
|
public function __construct(
|
|
private readonly NotificationService $notifications,
|
|
private readonly CreatorStudioPreferenceService $preferences,
|
|
) {
|
|
}
|
|
|
|
public function recent(User $user, int $limit = 12): array
|
|
{
|
|
return $this->feed($user)
|
|
->take($limit)
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
public function feed(User $user)
|
|
{
|
|
return $this->mergedFeed($user)
|
|
->sortByDesc(fn (array $item): int => $this->timestamp($item['created_at'] ?? null))
|
|
->values();
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $filters
|
|
*/
|
|
public function list(User $user, array $filters = []): array
|
|
{
|
|
$type = $this->normalizeType((string) ($filters['type'] ?? 'all'));
|
|
$module = $this->normalizeModule((string) ($filters['module'] ?? 'all'));
|
|
$q = trim((string) ($filters['q'] ?? ''));
|
|
$page = max(1, (int) ($filters['page'] ?? 1));
|
|
$perPage = min(max((int) ($filters['per_page'] ?? 24), 12), 48);
|
|
$preferences = $this->preferences->forUser($user);
|
|
|
|
$items = $this->feed($user);
|
|
|
|
if ($type !== 'all') {
|
|
$items = $items->where('type', $type)->values();
|
|
}
|
|
|
|
if ($module !== 'all') {
|
|
$items = $items->where('module', $module)->values();
|
|
}
|
|
|
|
if ($q !== '') {
|
|
$needle = mb_strtolower($q);
|
|
$items = $items->filter(function (array $item) use ($needle): bool {
|
|
return collect([
|
|
$item['title'] ?? '',
|
|
$item['body'] ?? '',
|
|
$item['actor']['name'] ?? '',
|
|
$item['module_label'] ?? '',
|
|
])->contains(fn ($value): bool => is_string($value) && str_contains(mb_strtolower($value), $needle));
|
|
})->values();
|
|
}
|
|
|
|
$items = $items->sortByDesc(fn (array $item): int => $this->timestamp($item['created_at'] ?? null))->values();
|
|
$total = $items->count();
|
|
$lastPage = max(1, (int) ceil($total / $perPage));
|
|
$page = min($page, $lastPage);
|
|
$lastReadAt = $preferences['activity_last_read_at'] ?? null;
|
|
$lastReadTimestamp = $this->timestamp($lastReadAt);
|
|
|
|
return [
|
|
'items' => $items->forPage($page, $perPage)->map(function (array $item) use ($lastReadTimestamp): array {
|
|
$item['is_new'] = $this->timestamp($item['created_at'] ?? null) > $lastReadTimestamp;
|
|
|
|
return $item;
|
|
})->values()->all(),
|
|
'meta' => [
|
|
'current_page' => $page,
|
|
'last_page' => $lastPage,
|
|
'per_page' => $perPage,
|
|
'total' => $total,
|
|
],
|
|
'filters' => [
|
|
'type' => $type,
|
|
'module' => $module,
|
|
'q' => $q,
|
|
],
|
|
'type_options' => [
|
|
['value' => 'all', 'label' => 'Everything'],
|
|
['value' => 'notification', 'label' => 'Notifications'],
|
|
['value' => 'comment', 'label' => 'Comments'],
|
|
['value' => 'follower', 'label' => 'Followers'],
|
|
],
|
|
'module_options' => [
|
|
['value' => 'all', 'label' => 'All content types'],
|
|
['value' => 'artworks', 'label' => 'Artworks'],
|
|
['value' => 'cards', 'label' => 'Cards'],
|
|
['value' => 'collections', 'label' => 'Collections'],
|
|
['value' => 'stories', 'label' => 'Stories'],
|
|
['value' => 'followers', 'label' => 'Followers'],
|
|
['value' => 'system', 'label' => 'System'],
|
|
],
|
|
'summary' => [
|
|
'unread_notifications' => (int) $user->unreadNotifications()->count(),
|
|
'last_read_at' => $lastReadAt,
|
|
'new_items' => $items->filter(fn (array $item): bool => $this->timestamp($item['created_at'] ?? null) > $lastReadTimestamp)->count(),
|
|
],
|
|
];
|
|
}
|
|
|
|
public function markAllRead(User $user): array
|
|
{
|
|
$this->notifications->markAllRead($user);
|
|
$updated = $this->preferences->update($user, [
|
|
'activity_last_read_at' => now()->toIso8601String(),
|
|
]);
|
|
|
|
return [
|
|
'ok' => true,
|
|
'activity_last_read_at' => $updated['activity_last_read_at'],
|
|
];
|
|
}
|
|
|
|
private function mergedFeed(User $user)
|
|
{
|
|
return collect($this->notificationItems($user))
|
|
->concat($this->commentItems($user))
|
|
->concat($this->followerItems($user));
|
|
}
|
|
|
|
private function notificationItems(User $user): array
|
|
{
|
|
return collect($this->notifications->listForUser($user, 1, 30)['data'] ?? [])
|
|
->map(fn (array $item): array => [
|
|
'id' => 'notification:' . $item['id'],
|
|
'type' => 'notification',
|
|
'module' => 'system',
|
|
'module_label' => 'Notification',
|
|
'title' => $item['message'],
|
|
'body' => $item['message'],
|
|
'created_at' => $item['created_at'],
|
|
'time_ago' => $item['time_ago'] ?? null,
|
|
'url' => $item['url'] ?? route('studio.activity'),
|
|
'actor' => $item['actor'] ?? null,
|
|
'read' => (bool) ($item['read'] ?? false),
|
|
])
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
private function commentItems(User $user): array
|
|
{
|
|
$artworkComments = DB::table('artwork_comments')
|
|
->join('artworks', 'artworks.id', '=', 'artwork_comments.artwork_id')
|
|
->join('users', 'users.id', '=', 'artwork_comments.user_id')
|
|
->leftJoin('user_profiles', 'user_profiles.user_id', '=', 'users.id')
|
|
->where('artworks.user_id', $user->id)
|
|
->whereNull('artwork_comments.deleted_at')
|
|
->orderByDesc('artwork_comments.created_at')
|
|
->limit(20)
|
|
->get([
|
|
'artwork_comments.id',
|
|
'artwork_comments.content as body',
|
|
'artwork_comments.created_at',
|
|
'users.id as actor_id',
|
|
'users.name as actor_name',
|
|
'users.username as actor_username',
|
|
'user_profiles.avatar_hash',
|
|
'artworks.title as item_title',
|
|
'artworks.slug as item_slug',
|
|
'artworks.id as item_id',
|
|
])
|
|
->map(fn ($row): array => [
|
|
'id' => 'comment:artworks:' . $row->id,
|
|
'type' => 'comment',
|
|
'module' => 'artworks',
|
|
'module_label' => 'Artwork comment',
|
|
'title' => 'New comment on ' . $row->item_title,
|
|
'body' => (string) $row->body,
|
|
'created_at' => $this->normalizeDate($row->created_at),
|
|
'time_ago' => null,
|
|
'url' => route('art.show', ['id' => $row->item_id, 'slug' => $row->item_slug]) . '#comment-' . $row->id,
|
|
'actor' => [
|
|
'id' => (int) $row->actor_id,
|
|
'name' => $row->actor_name ?: $row->actor_username ?: 'Creator',
|
|
'username' => $row->actor_username,
|
|
'avatar_url' => AvatarUrl::forUser((int) $row->actor_id, $row->avatar_hash, 64),
|
|
],
|
|
]);
|
|
|
|
$cardComments = NovaCardComment::query()
|
|
->with(['user.profile', 'card'])
|
|
->whereNull('deleted_at')
|
|
->whereHas('card', fn ($query) => $query->where('user_id', $user->id))
|
|
->latest('created_at')
|
|
->limit(20)
|
|
->get()
|
|
->map(fn (NovaCardComment $comment): array => [
|
|
'id' => 'comment:cards:' . $comment->id,
|
|
'type' => 'comment',
|
|
'module' => 'cards',
|
|
'module_label' => 'Card comment',
|
|
'title' => 'New comment on ' . ($comment->card?->title ?? 'card'),
|
|
'body' => (string) $comment->body,
|
|
'created_at' => $comment->created_at?->toIso8601String(),
|
|
'time_ago' => $comment->created_at?->diffForHumans(),
|
|
'url' => $comment->card ? $comment->card->publicUrl() . '#comment-' . $comment->id : route('studio.activity'),
|
|
'actor' => $comment->user ? [
|
|
'id' => (int) $comment->user->id,
|
|
'name' => $comment->user->name ?: $comment->user->username ?: 'Creator',
|
|
'username' => $comment->user->username,
|
|
'avatar_url' => AvatarUrl::forUser((int) $comment->user->id, $comment->user->profile?->avatar_hash, 64),
|
|
] : null,
|
|
]);
|
|
|
|
$collectionComments = CollectionComment::query()
|
|
->with(['user.profile', 'collection'])
|
|
->whereNull('deleted_at')
|
|
->whereHas('collection', fn ($query) => $query->where('user_id', $user->id))
|
|
->latest('created_at')
|
|
->limit(20)
|
|
->get()
|
|
->map(fn (CollectionComment $comment): array => [
|
|
'id' => 'comment:collections:' . $comment->id,
|
|
'type' => 'comment',
|
|
'module' => 'collections',
|
|
'module_label' => 'Collection comment',
|
|
'title' => 'New comment on ' . ($comment->collection?->title ?? 'collection'),
|
|
'body' => (string) $comment->body,
|
|
'created_at' => $comment->created_at?->toIso8601String(),
|
|
'time_ago' => $comment->created_at?->diffForHumans(),
|
|
'url' => $comment->collection ? route('settings.collections.show', ['collection' => $comment->collection->id]) : route('studio.activity'),
|
|
'actor' => $comment->user ? [
|
|
'id' => (int) $comment->user->id,
|
|
'name' => $comment->user->name ?: $comment->user->username ?: 'Creator',
|
|
'username' => $comment->user->username,
|
|
'avatar_url' => AvatarUrl::forUser((int) $comment->user->id, $comment->user->profile?->avatar_hash, 64),
|
|
] : null,
|
|
]);
|
|
|
|
$storyComments = StoryComment::query()
|
|
->with(['user.profile', 'story'])
|
|
->whereNull('deleted_at')
|
|
->whereHas('story', fn ($query) => $query->where('creator_id', $user->id))
|
|
->latest('created_at')
|
|
->limit(20)
|
|
->get()
|
|
->map(fn (StoryComment $comment): array => [
|
|
'id' => 'comment:stories:' . $comment->id,
|
|
'type' => 'comment',
|
|
'module' => 'stories',
|
|
'module_label' => 'Story comment',
|
|
'title' => 'New comment on ' . ($comment->story?->title ?? 'story'),
|
|
'body' => (string) ($comment->raw_content ?: $comment->content),
|
|
'created_at' => $comment->created_at?->toIso8601String(),
|
|
'time_ago' => $comment->created_at?->diffForHumans(),
|
|
'url' => $comment->story ? route('stories.show', ['slug' => $comment->story->slug]) . '#comment-' . $comment->id : route('studio.activity'),
|
|
'actor' => $comment->user ? [
|
|
'id' => (int) $comment->user->id,
|
|
'name' => $comment->user->name ?: $comment->user->username ?: 'Creator',
|
|
'username' => $comment->user->username,
|
|
'avatar_url' => AvatarUrl::forUser((int) $comment->user->id, $comment->user->profile?->avatar_hash, 64),
|
|
] : null,
|
|
]);
|
|
|
|
return $artworkComments
|
|
->concat($cardComments)
|
|
->concat($collectionComments)
|
|
->concat($storyComments)
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
private function followerItems(User $user): array
|
|
{
|
|
return DB::table('user_followers as uf')
|
|
->join('users as follower', 'follower.id', '=', 'uf.follower_id')
|
|
->leftJoin('user_profiles as profile', 'profile.user_id', '=', 'follower.id')
|
|
->where('uf.user_id', $user->id)
|
|
->whereNull('follower.deleted_at')
|
|
->orderByDesc('uf.created_at')
|
|
->limit(20)
|
|
->get([
|
|
'uf.created_at',
|
|
'follower.id',
|
|
'follower.username',
|
|
'follower.name',
|
|
'profile.avatar_hash',
|
|
])
|
|
->map(fn ($row): array => [
|
|
'id' => 'follower:' . $row->id . ':' . strtotime((string) $row->created_at),
|
|
'type' => 'follower',
|
|
'module' => 'followers',
|
|
'module_label' => 'Follower',
|
|
'title' => ($row->name ?: $row->username ?: 'Someone') . ' followed you',
|
|
'body' => 'New audience activity in Creator Studio.',
|
|
'created_at' => $this->normalizeDate($row->created_at),
|
|
'time_ago' => null,
|
|
'url' => '/@' . strtolower((string) $row->username),
|
|
'actor' => [
|
|
'id' => (int) $row->id,
|
|
'name' => $row->name ?: $row->username ?: 'Creator',
|
|
'username' => $row->username,
|
|
'avatar_url' => AvatarUrl::forUser((int) $row->id, $row->avatar_hash, 64),
|
|
],
|
|
])
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
private function normalizeType(string $type): string
|
|
{
|
|
return in_array($type, ['all', 'notification', 'comment', 'follower'], true)
|
|
? $type
|
|
: 'all';
|
|
}
|
|
|
|
private function normalizeModule(string $module): string
|
|
{
|
|
return in_array($module, ['all', 'artworks', 'cards', 'collections', 'stories', 'followers', 'system'], true)
|
|
? $module
|
|
: 'all';
|
|
}
|
|
|
|
private function timestamp(mixed $value): int
|
|
{
|
|
if (! is_string($value) || $value === '') {
|
|
return 0;
|
|
}
|
|
|
|
return strtotime($value) ?: 0;
|
|
}
|
|
|
|
private function normalizeDate(mixed $value): ?string
|
|
{
|
|
if ($value instanceof \DateTimeInterface) {
|
|
return $value->format(DATE_ATOM);
|
|
}
|
|
|
|
if (is_string($value) && $value !== '') {
|
|
return $value;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
} |