buildPayload($user, false); } public function ownerPayloadForUser(User $user): array { return $this->buildPayload($user, true); } private function buildPayload(User $user, bool $includeOwnerContext): array { $submissions = $this->submissionsForUser($user); $rewardGrants = $this->rewardGrantsForUser($user); $challengeOutcomes = $this->challengeOutcomesForUser($user); $challengeWorldMap = $this->challengeWorldMap($challengeOutcomes->pluck('group_challenge_id')->unique()->values()); $entries = []; $hiddenPublicEntries = 0; foreach ($rewardGrants as $grant) { if (! $this->grantQualifiesForPublicHistory($grant, $submissions)) { $hiddenPublicEntries++; continue; } $this->addRecognition( $entries, $grant->world, $grant->reward_type->value, $grant->artwork, $grant->granted_at, $grant->grant_source === 'challenge' ? $this->challengeContextForWorld($grant->world) : null, 'reward' ); } $liveSubmissions = $submissions ->filter(fn (WorldSubmission $submission): bool => $this->submissionQualifiesForPublicHistory($submission)); foreach ($liveSubmissions as $submission) { $recognitionKey = $submission->is_featured ? WorldRewardType::Featured->value : WorldRewardType::Participant->value; $this->addRecognition( $entries, $submission->world, $recognitionKey, $submission->artwork, $submission->featured_at ?? $submission->reviewed_at ?? $submission->created_at, $this->challengeContextForWorld($submission->world), 'participation' ); } foreach ($challengeOutcomes as $outcome) { $recognitionKey = $this->recognitionKeyForOutcome($outcome->outcome_type); if ($recognitionKey === null || ! $this->artworkIsPubliclyVisible($outcome->artwork)) { $hiddenPublicEntries++; continue; } $worlds = $challengeWorldMap->get((int) $outcome->group_challenge_id, Collection::make()); if ($worlds->isEmpty()) { continue; } foreach ($worlds as $world) { $this->addRecognition( $entries, $world, $recognitionKey, $outcome->artwork, $outcome->awarded_at ?? $outcome->created_at, $this->challengeContextFromChallenge($outcome->challenge), 'challenge_outcome' ); } } $normalizedEntries = Collection::make($entries) ->map(fn (array $entry): array => $this->normalizeEntry($entry)) ->sort(function (array $left, array $right): int { if ($left['occurred_at'] !== $right['occurred_at']) { return strcmp((string) $right['occurred_at'], (string) $left['occurred_at']); } if ($left['primary_recognition']['priority'] !== $right['primary_recognition']['priority']) { return $left['primary_recognition']['priority'] <=> $right['primary_recognition']['priority']; } return strcmp((string) $left['world']['title'], (string) $right['world']['title']); }) ->values(); $yearValues = $normalizedEntries ->pluck('world.edition_year') ->filter(fn ($year): bool => is_int($year) || ctype_digit((string) $year)) ->map(fn ($year): int => (int) $year) ->values(); $worldAppearances = $normalizedEntries->count(); $highlights = $normalizedEntries->take(3)->values(); $mostRecent = $normalizedEntries->first(); return [ 'summary' => [ 'available' => $normalizedEntries->isNotEmpty(), 'world_appearances' => $worldAppearances, 'worlds_joined' => $worldAppearances, 'featured_appearances' => $normalizedEntries->filter(fn (array $entry): bool => in_array('featured', $entry['recognition_keys'], true))->count(), 'winner_appearances' => $normalizedEntries->filter(fn (array $entry): bool => in_array('winner', $entry['recognition_keys'], true))->count(), 'finalist_appearances' => $normalizedEntries->filter(fn (array $entry): bool => in_array('finalist', $entry['recognition_keys'], true))->count(), 'spotlight_appearances' => $normalizedEntries->filter(fn (array $entry): bool => in_array('spotlight', $entry['recognition_keys'], true))->count(), 'finalist_winner_appearances' => $normalizedEntries->filter(fn (array $entry): bool => in_array('winner', $entry['recognition_keys'], true) || in_array('finalist', $entry['recognition_keys'], true))->count(), 'active_year_span' => $this->yearSpanPayload($yearValues), 'most_recent_world_activity' => $mostRecent ? [ 'world_title' => $mostRecent['world']['title'], 'primary_recognition' => $mostRecent['primary_recognition'], 'recognition_label' => $mostRecent['primary_recognition']['label'], 'world_url' => $mostRecent['world']['url'], 'occurred_at' => $mostRecent['occurred_at'], ] : null, ], 'highlights' => $highlights->all(), 'entries' => $normalizedEntries->all(), 'owner_context' => $includeOwnerContext ? [ 'pending_submissions' => $submissions->where('status', WorldSubmission::STATUS_PENDING)->count(), 'removed_or_blocked_submissions' => $submissions->filter(fn (WorldSubmission $submission): bool => in_array((string) $submission->status, [WorldSubmission::STATUS_REMOVED, WorldSubmission::STATUS_BLOCKED], true))->count(), 'hidden_public_entries' => $hiddenPublicEntries, ] : null, 'filters' => [ 'default_order' => 'recent_first', 'groupable_by' => ['year', 'world_family', 'recognition_type'], ], ]; } private function submissionsForUser(User $user): Collection { return WorldSubmission::query() ->with([ 'world:id,title,slug,type,recurrence_key,edition_year,linked_challenge_id,status,published_at,deleted_at', 'world.linkedChallenge.group:id,name,slug,visibility,status', 'artwork:id,user_id,title,slug,hash,thumb_ext,is_public,visibility,is_approved,published_at,deleted_at', ]) ->where('submitted_by_user_id', (int) $user->id) ->whereHas('world', fn ($builder) => $builder->publiclyVisible()) ->whereHas('artwork', fn ($builder) => $builder->where('user_id', (int) $user->id)) ->get(); } private function rewardGrantsForUser(User $user): Collection { return WorldRewardGrant::query() ->with([ 'world:id,title,slug,type,recurrence_key,edition_year,linked_challenge_id,status,published_at,deleted_at', 'world.linkedChallenge.group:id,name,slug,visibility,status', 'artwork:id,user_id,title,slug,hash,thumb_ext,is_public,visibility,is_approved,published_at,deleted_at', 'worldSubmission:id,world_id,artwork_id,status,is_featured,featured_at,reviewed_at,created_at', ]) ->where('user_id', (int) $user->id) ->whereHas('world', fn ($builder) => $builder->publiclyVisible()) ->orderByDesc('granted_at') ->orderByDesc('id') ->get(); } private function challengeOutcomesForUser(User $user): Collection { return GroupChallengeOutcome::query() ->with([ 'challenge.group:id,name,slug,visibility,status', 'artwork:id,user_id,title,slug,hash,thumb_ext,is_public,visibility,is_approved,published_at,deleted_at', ]) ->where('user_id', (int) $user->id) ->whereHas('artwork', fn ($builder) => $builder->where('user_id', (int) $user->id)) ->get(); } private function challengeWorldMap(Collection $challengeIds): Collection { if ($challengeIds->isEmpty()) { return Collection::make(); } $map = Collection::make(); World::query() ->with('linkedChallenge.group') ->publiclyVisible() ->whereIn('linked_challenge_id', $challengeIds->all()) ->get() ->each(function (World $world) use (&$map): void { $challengeId = (int) ($world->linked_challenge_id ?? 0); if ($challengeId <= 0) { return; } $items = $map->get($challengeId, Collection::make()); $map->put($challengeId, $items->push($world)->unique('id')->values()); }); WorldRelation::query() ->with(['world.linkedChallenge.group']) ->where('related_type', WorldRelation::TYPE_CHALLENGE) ->whereIn('related_id', $challengeIds->all()) ->whereHas('world', fn ($builder) => $builder->publiclyVisible()) ->get() ->each(function (WorldRelation $relation) use (&$map): void { $challengeId = (int) $relation->related_id; $world = $relation->world; if (! $world) { return; } $items = $map->get($challengeId, Collection::make()); $map->put($challengeId, $items->push($world)->unique('id')->values()); }); return $map; } private function grantQualifiesForPublicHistory(WorldRewardGrant $grant, Collection $submissions): bool { if (! $grant->world || ! $this->artworkIsPubliclyVisible($grant->artwork)) { return false; } return match ($grant->reward_type) { WorldRewardType::Participant => $submissions->contains(fn (WorldSubmission $submission): bool => (int) $submission->world_id === (int) $grant->world_id && $this->submissionQualifiesForPublicHistory($submission)), WorldRewardType::Featured => $submissions->contains(fn (WorldSubmission $submission): bool => (int) $submission->world_id === (int) $grant->world_id && $this->submissionQualifiesForPublicHistory($submission) && (bool) $submission->is_featured), default => true, }; } private function submissionQualifiesForPublicHistory(WorldSubmission $submission): bool { return $submission->world !== null && (string) $submission->status === WorldSubmission::STATUS_LIVE && $this->artworkIsPubliclyVisible($submission->artwork); } private function artworkIsPubliclyVisible(?Artwork $artwork): bool { if (! $artwork) { return false; } return $artwork->deleted_at === null && (bool) $artwork->is_approved && (bool) $artwork->is_public && $artwork->published_at !== null && $artwork->published_at->isPast() && in_array((string) ($artwork->visibility ?? Artwork::VISIBILITY_PUBLIC), ['', Artwork::VISIBILITY_PUBLIC], true); } private function recognitionKeyForOutcome(string $outcomeType): ?string { return match ($outcomeType) { GroupChallengeOutcome::TYPE_WINNER => 'winner', GroupChallengeOutcome::TYPE_FINALIST => 'finalist', GroupChallengeOutcome::TYPE_FEATURED => 'featured', GroupChallengeOutcome::TYPE_RUNNER_UP => 'runner_up', GroupChallengeOutcome::TYPE_HONORABLE_MENTION => 'honorable_mention', default => null, }; } private function addRecognition(array &$entries, World $world, string $recognitionKey, ?Artwork $artwork, ?\DateTimeInterface $occurredAt, ?array $challengeContext, string $sourceType): void { $worldId = (int) $world->id; $timestamp = $occurredAt?->format(DATE_ATOM) ?? date(DATE_ATOM); if (! array_key_exists($worldId, $entries)) { $entries[$worldId] = [ 'world' => $world, 'recognitions' => [], 'occurred_at' => $timestamp, ]; } if (! array_key_exists($recognitionKey, $entries[$worldId]['recognitions'])) { $entries[$worldId]['recognitions'][$recognitionKey] = [ 'recognition' => $this->recognitionPayload($recognitionKey), 'linked_artwork' => $this->artworkPayload($artwork), 'challenge' => $challengeContext, 'occurred_at' => $timestamp, 'source_types' => [$sourceType], ]; } else { $current = $entries[$worldId]['recognitions'][$recognitionKey]; $entries[$worldId]['recognitions'][$recognitionKey] = [ 'recognition' => $current['recognition'], 'linked_artwork' => $current['linked_artwork'] ?? $this->artworkPayload($artwork), 'challenge' => $current['challenge'] ?? $challengeContext, 'occurred_at' => max((string) $current['occurred_at'], $timestamp), 'source_types' => array_values(array_unique([...$current['source_types'], $sourceType])), ]; } if ($timestamp > (string) $entries[$worldId]['occurred_at']) { $entries[$worldId]['occurred_at'] = $timestamp; } } private function normalizeEntry(array $entry): array { /** @var World $world */ $world = $entry['world']; $recognitions = Collection::make($entry['recognitions']) ->sort(function (array $left, array $right): int { if ($left['recognition']['priority'] !== $right['recognition']['priority']) { return $left['recognition']['priority'] <=> $right['recognition']['priority']; } return strcmp((string) $right['occurred_at'], (string) $left['occurred_at']); }) ->values(); $primary = $recognitions->first(); $linkedArtwork = $primary['linked_artwork'] ?? $recognitions->pluck('linked_artwork')->first(fn ($item) => $item !== null); $challenge = $primary['challenge'] ?? $recognitions->pluck('challenge')->first(fn ($item) => $item !== null); return [ 'id' => 'world-history-' . (int) $world->id, 'world' => [ 'id' => (int) $world->id, 'title' => (string) $world->title, 'slug' => (string) $world->slug, 'url' => $world->publicUrl(), 'type' => (string) $world->type, 'type_label' => Str::headline((string) $world->type), 'edition_year' => $world->edition_year ? (int) $world->edition_year : null, 'family_key' => (string) ($world->recurrence_key ?: 'world-' . $world->id), 'family_label' => $this->familyLabelForWorld($world), ], 'primary_recognition' => $primary['recognition'], 'recognitions' => $recognitions->map(fn (array $recognition): array => [ ...$recognition['recognition'], 'source_types' => $recognition['source_types'], ])->all(), 'recognition_keys' => $recognitions->map(fn (array $recognition): string => (string) $recognition['recognition']['key'])->values()->all(), 'linked_artwork' => $linkedArtwork, 'challenge' => $challenge, 'occurred_at' => (string) $entry['occurred_at'], 'source_types' => $recognitions->flatMap(fn (array $recognition): array => $recognition['source_types'])->unique()->values()->all(), ]; } private function artworkPayload(?Artwork $artwork): ?array { if (! $artwork || ! $this->artworkIsPubliclyVisible($artwork)) { return null; } return [ 'id' => (int) $artwork->id, 'title' => (string) ($artwork->title ?: 'Untitled artwork'), 'url' => route('art.show', ['id' => (int) $artwork->id, 'slug' => $artwork->slug ?: Str::slug((string) $artwork->title)]), 'thumbnail_url' => $artwork->thumb_url, ]; } private function challengeContextForWorld(?World $world): ?array { if (! $world || ! $world->linkedChallenge || ! $world->linkedChallenge->canBeViewedBy(null)) { return null; } return $this->challengeContextFromChallenge($world->linkedChallenge); } private function challengeContextFromChallenge(?GroupChallenge $challenge): ?array { if (! $challenge || ! $challenge->group || ! $challenge->canBeViewedBy(null)) { return null; } return [ 'id' => (int) $challenge->id, 'title' => (string) $challenge->title, 'url' => route('groups.challenges.show', ['group' => $challenge->group, 'challenge' => $challenge]), 'group_name' => (string) $challenge->group->name, ]; } private function recognitionPayload(string $recognitionKey): array { return match ($recognitionKey) { 'winner' => ['key' => 'winner', 'label' => 'Winner', 'tone' => 'emerald', 'priority' => 0], 'finalist' => ['key' => 'finalist', 'label' => 'Finalist', 'tone' => 'violet', 'priority' => 1], 'featured' => ['key' => 'featured', 'label' => 'Featured', 'tone' => 'amber', 'priority' => 2], 'spotlight' => ['key' => 'spotlight', 'label' => 'Spotlight', 'tone' => 'rose', 'priority' => 3], 'participant' => ['key' => 'participant', 'label' => 'Participant', 'tone' => 'sky', 'priority' => 4], 'runner_up' => ['key' => 'runner_up', 'label' => 'Runner-up', 'tone' => 'slate', 'priority' => 5], 'honorable_mention' => ['key' => 'honorable_mention', 'label' => 'Honorable Mention', 'tone' => 'slate', 'priority' => 6], default => ['key' => $recognitionKey, 'label' => Str::headline(str_replace('_', ' ', $recognitionKey)), 'tone' => 'slate', 'priority' => 7], }; } private function familyLabelForWorld(World $world): string { if ($world->recurrence_key) { return Str::headline(str_replace('-', ' ', (string) $world->recurrence_key)); } if ($world->edition_year) { return trim((string) preg_replace('/\s+' . preg_quote((string) $world->edition_year, '/') . '$/', '', (string) $world->title)); } return (string) $world->title; } private function yearSpanPayload(Collection $years): ?array { if ($years->isEmpty()) { return null; } $start = (int) $years->min(); $end = (int) $years->max(); return [ 'start' => $start, 'end' => $end, 'label' => $start === $end ? (string) $start : sprintf('%d-%d', $start, $end), ]; } }