create([ 'group_id' => (int) $group->id, 'type' => $type, 'visibility' => $visibility, 'actor_user_id' => $actor?->id, 'subject_type' => $subjectType, 'subject_id' => $subjectId, 'headline' => $headline, 'summary' => $summary, 'is_pinned' => false, 'occurred_at' => now(), ]); } public function pin(GroupActivityItem $item, User $actor, bool $isPinned = true): GroupActivityItem { $item->forceFill([ 'is_pinned' => $isPinned, ])->save(); app(GroupHistoryService::class)->record( $item->group, $actor, $isPinned ? 'activity_pinned' : 'activity_unpinned', sprintf('%s group activity item.', $isPinned ? 'Pinned' : 'Unpinned'), 'group_activity_item', (int) $item->id, ['is_pinned' => ! $isPinned], ['is_pinned' => $isPinned], ); return $item->fresh(['actor']); } public function publicFeed(Group $group, int $limit = 8): array { return $this->mapItems( GroupActivityItem::query() ->with('actor:id,name,username') ->where('group_id', $group->id) ->where('visibility', GroupActivityItem::VISIBILITY_PUBLIC) ->orderByDesc('is_pinned') ->orderByDesc('occurred_at') ->limit(max(1, min(24, $limit))) ->get(), $group ); } public function studioFeed(Group $group, User $viewer, int $limit = 20): array { if (! $group->canViewStudio($viewer)) { return []; } return $this->mapItems( GroupActivityItem::query() ->with('actor:id,name,username') ->where('group_id', $group->id) ->orderByDesc('is_pinned') ->orderByDesc('occurred_at') ->limit(max(1, min(50, $limit))) ->get(), $group ); } private function mapItems(Collection $items, Group $group): array { $subjects = $this->loadSubjects($items); return $items->map(function (GroupActivityItem $item) use ($group, $subjects): array { $subject = $subjects[$item->subject_type][$item->subject_id] ?? null; return [ 'id' => (int) $item->id, 'type' => (string) $item->type, 'visibility' => (string) $item->visibility, 'headline' => (string) $item->headline, 'summary' => $item->summary, 'is_pinned' => (bool) $item->is_pinned, 'occurred_at' => $item->occurred_at?->toISOString(), 'actor' => $item->actor ? [ 'id' => (int) $item->actor->id, 'name' => $item->actor->name, 'username' => $item->actor->username, ] : null, 'subject' => $subject ? [ 'type' => (string) $item->subject_type, 'id' => (int) $item->subject_id, 'title' => $subject->title ?? null, 'url' => $this->subjectUrl($group, (string) $item->subject_type, $subject), ] : null, ]; })->values()->all(); } private function loadSubjects(Collection $items): array { $grouped = $items ->filter(fn (GroupActivityItem $item): bool => $item->subject_id !== null) ->groupBy('subject_type') ->map(fn (Collection $chunk): array => $chunk->pluck('subject_id')->map(fn ($id): int => (int) $id)->unique()->values()->all()); return [ 'artwork' => Artwork::query()->whereIn('id', $grouped->get('artwork', []))->get()->keyBy('id')->all(), 'group_post' => GroupPost::query()->whereIn('id', $grouped->get('group_post', []))->get()->keyBy('id')->all(), 'group_project' => GroupProject::query()->whereIn('id', $grouped->get('group_project', []))->get()->keyBy('id')->all(), 'group_release' => GroupRelease::query()->whereIn('id', $grouped->get('group_release', []))->get()->keyBy('id')->all(), 'group_challenge' => GroupChallenge::query()->whereIn('id', $grouped->get('group_challenge', []))->get()->keyBy('id')->all(), 'group_event' => GroupEvent::query()->whereIn('id', $grouped->get('group_event', []))->get()->keyBy('id')->all(), 'group_asset' => GroupAsset::query()->whereIn('id', $grouped->get('group_asset', []))->get()->keyBy('id')->all(), ]; } private function subjectUrl(Group $group, string $subjectType, object $subject): ?string { return match ($subjectType) { 'artwork' => route('art.show', ['id' => $subject->id, 'slug' => $subject->slug ?: $subject->id]), 'group_post' => route('groups.posts.show', ['group' => $group, 'post' => $subject]), 'group_project' => route('groups.projects.show', ['group' => $group, 'project' => $subject]), 'group_release' => route('groups.releases.show', ['group' => $group, 'release' => $subject]), 'group_challenge' => route('groups.challenges.show', ['group' => $group, 'challenge' => $subject]), 'group_event' => route('groups.events.show', ['group' => $group, 'event' => $subject]), 'group_asset' => route('groups.assets.download', ['group' => $group, 'asset' => $subject]), default => null, }; } }