feat: ship creator journey v2 and profile updates
This commit is contained in:
@@ -4,15 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Jobs\RecalculateArtworkMedalStatsJob;
|
||||
use App\Models\ArtworkAward;
|
||||
use App\Services\ArtworkAwardService;
|
||||
use App\Services\UserStatsService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ArtworkAwardObserver
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ArtworkAwardService $service,
|
||||
private readonly UserStatsService $userStats,
|
||||
) {}
|
||||
|
||||
@@ -36,12 +35,7 @@ class ArtworkAwardObserver
|
||||
|
||||
private function refresh(ArtworkAward $award): void
|
||||
{
|
||||
$this->service->recalcStats($award->artwork_id);
|
||||
|
||||
$artwork = $award->artwork;
|
||||
if ($artwork) {
|
||||
$this->service->syncToSearch($artwork);
|
||||
}
|
||||
RecalculateArtworkMedalStatsJob::dispatchSync((int) $award->artwork_id);
|
||||
}
|
||||
|
||||
private function trackCreatorStats(ArtworkAward $award, int $delta): void
|
||||
|
||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\ArtworkComment;
|
||||
use App\Services\ArtworkStatsService;
|
||||
use App\Services\Profile\CreatorJourneyService;
|
||||
use App\Services\UserStatsService;
|
||||
use App\Services\UserMentionSyncService;
|
||||
use App\Services\XPService;
|
||||
@@ -17,7 +19,9 @@ use Illuminate\Support\Facades\DB;
|
||||
class ArtworkCommentObserver
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ArtworkStatsService $artworkStats,
|
||||
private readonly UserStatsService $userStats,
|
||||
private readonly CreatorJourneyService $journeys,
|
||||
private readonly UserMentionSyncService $mentionSync,
|
||||
private readonly XPService $xp,
|
||||
) {}
|
||||
@@ -27,6 +31,7 @@ class ArtworkCommentObserver
|
||||
$creatorId = $this->creatorId($comment->artwork_id);
|
||||
if ($creatorId) {
|
||||
$this->userStats->incrementCommentsReceived($creatorId);
|
||||
$this->journeys->requestRebuild($creatorId);
|
||||
}
|
||||
|
||||
// The commenter is "active"
|
||||
@@ -34,6 +39,7 @@ class ArtworkCommentObserver
|
||||
$this->userStats->setLastActiveAt($comment->user_id);
|
||||
$this->xp->awardCommentCreated((int) $comment->user_id, (int) $comment->id, 'artwork');
|
||||
$this->mentionSync->syncForComment($comment);
|
||||
$this->artworkStats->syncEngagementCounts((int) $comment->artwork_id);
|
||||
}
|
||||
|
||||
public function updated(ArtworkComment $comment): void
|
||||
@@ -49,9 +55,11 @@ class ArtworkCommentObserver
|
||||
$creatorId = $this->creatorId($comment->artwork_id);
|
||||
if ($creatorId) {
|
||||
$this->userStats->decrementCommentsReceived($creatorId);
|
||||
$this->journeys->requestRebuild($creatorId);
|
||||
}
|
||||
|
||||
$this->mentionSync->deleteForComment((int) $comment->id);
|
||||
$this->artworkStats->syncEngagementCounts((int) $comment->artwork_id);
|
||||
}
|
||||
|
||||
/** Hard delete after soft delete — already decremented; nothing to do. */
|
||||
@@ -63,15 +71,23 @@ class ArtworkCommentObserver
|
||||
$creatorId = $this->creatorId($comment->artwork_id);
|
||||
if ($creatorId) {
|
||||
$this->userStats->decrementCommentsReceived($creatorId);
|
||||
$this->journeys->requestRebuild($creatorId);
|
||||
}
|
||||
}
|
||||
|
||||
$this->mentionSync->deleteForComment((int) $comment->id);
|
||||
$this->artworkStats->syncEngagementCounts((int) $comment->artwork_id);
|
||||
}
|
||||
|
||||
public function restored(ArtworkComment $comment): void
|
||||
{
|
||||
$this->mentionSync->syncForComment($comment);
|
||||
$this->artworkStats->syncEngagementCounts((int) $comment->artwork_id);
|
||||
|
||||
$creatorId = $this->creatorId($comment->artwork_id);
|
||||
if ($creatorId) {
|
||||
$this->journeys->requestRebuild($creatorId);
|
||||
}
|
||||
}
|
||||
|
||||
private function creatorId(int $artworkId): ?int
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Observers;
|
||||
use App\Jobs\RecComputeSimilarByBehaviorJob;
|
||||
use App\Jobs\RecComputeSimilarHybridJob;
|
||||
use App\Models\ArtworkFavourite;
|
||||
use App\Services\Profile\CreatorJourneyService;
|
||||
use App\Services\UserStatsService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
@@ -18,6 +19,7 @@ class ArtworkFavouriteObserver
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserStatsService $userStats,
|
||||
private readonly CreatorJourneyService $journeys,
|
||||
) {}
|
||||
|
||||
public function created(ArtworkFavourite $favourite): void
|
||||
@@ -25,6 +27,7 @@ class ArtworkFavouriteObserver
|
||||
$creatorId = $this->creatorId($favourite->artwork_id);
|
||||
if ($creatorId) {
|
||||
$this->userStats->incrementFavoritesReceived($creatorId);
|
||||
$this->journeys->requestRebuild($creatorId);
|
||||
}
|
||||
|
||||
// §7.5 On-demand: recompute behavior similarity when artwork reaches threshold
|
||||
@@ -36,6 +39,7 @@ class ArtworkFavouriteObserver
|
||||
$creatorId = $this->creatorId($favourite->artwork_id);
|
||||
if ($creatorId) {
|
||||
$this->userStats->decrementFavoritesReceived($creatorId);
|
||||
$this->journeys->requestRebuild($creatorId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
63
app/Observers/ArtworkFeatureObserver.php
Normal file
63
app/Observers/ArtworkFeatureObserver.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkFeature;
|
||||
use App\Services\HomepageService;
|
||||
use App\Services\Profile\CreatorJourneyService;
|
||||
|
||||
final class ArtworkFeatureObserver
|
||||
{
|
||||
public function __construct(
|
||||
private readonly HomepageService $homepage,
|
||||
private readonly CreatorJourneyService $journeys,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function created(ArtworkFeature $feature): void
|
||||
{
|
||||
$this->homepage->clearFeaturedAndMedalCaches();
|
||||
$this->queueCreatorRebuild($feature);
|
||||
}
|
||||
|
||||
public function updated(ArtworkFeature $feature): void
|
||||
{
|
||||
$this->homepage->clearFeaturedAndMedalCaches();
|
||||
$this->queueCreatorRebuild($feature);
|
||||
}
|
||||
|
||||
public function deleted(ArtworkFeature $feature): void
|
||||
{
|
||||
$this->homepage->clearFeaturedAndMedalCaches();
|
||||
$this->queueCreatorRebuild($feature);
|
||||
}
|
||||
|
||||
public function restored(ArtworkFeature $feature): void
|
||||
{
|
||||
$this->homepage->clearFeaturedAndMedalCaches();
|
||||
$this->queueCreatorRebuild($feature);
|
||||
}
|
||||
|
||||
public function forceDeleted(ArtworkFeature $feature): void
|
||||
{
|
||||
$this->homepage->clearFeaturedAndMedalCaches();
|
||||
$this->queueCreatorRebuild($feature);
|
||||
}
|
||||
|
||||
private function queueCreatorRebuild(ArtworkFeature $feature): void
|
||||
{
|
||||
$artwork = $feature->relationLoaded('artwork')
|
||||
? $feature->artwork
|
||||
: Artwork::withTrashed()->find($feature->artwork_id);
|
||||
|
||||
if (! $artwork) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->journeys->requestRebuild((int) $artwork->user_id);
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,11 @@ use App\Jobs\RecComputeSimilarByTagsJob;
|
||||
use App\Jobs\RecComputeSimilarHybridJob;
|
||||
use App\Jobs\Posts\AutoUploadPostJob;
|
||||
use App\Services\ArtworkSearchIndexer;
|
||||
use App\Services\HomepageService;
|
||||
use App\Services\Profile\CreatorJourneyService;
|
||||
use App\Services\UserStatsService;
|
||||
use App\Services\XPService;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* Syncs artwork documents to Meilisearch on every relevant model event.
|
||||
@@ -25,6 +28,8 @@ class ArtworkObserver
|
||||
private readonly ArtworkSearchIndexer $indexer,
|
||||
private readonly UserStatsService $userStats,
|
||||
private readonly XPService $xp,
|
||||
private readonly HomepageService $homepage,
|
||||
private readonly CreatorJourneyService $journeys,
|
||||
) {}
|
||||
|
||||
/** New artwork created — index; bump uploadscount + last_upload_at. */
|
||||
@@ -33,6 +38,11 @@ class ArtworkObserver
|
||||
$this->indexer->index($artwork);
|
||||
$this->userStats->incrementUploads($artwork->user_id);
|
||||
$this->userStats->setLastUploadAt($artwork->user_id, $artwork->created_at);
|
||||
$this->journeys->requestRebuild((int) $artwork->user_id);
|
||||
|
||||
if ($artwork->is_public && $artwork->is_approved && $artwork->published_at !== null) {
|
||||
$this->bumpExploreCacheVersion();
|
||||
}
|
||||
|
||||
if ($artwork->published_at !== null) {
|
||||
$this->xp->awardArtworkPublished((int) $artwork->user_id, (int) $artwork->id);
|
||||
@@ -75,6 +85,18 @@ class ArtworkObserver
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->shouldClearFeaturedCaches($artwork)) {
|
||||
$this->homepage->clearFeaturedAndMedalCaches();
|
||||
}
|
||||
|
||||
if ($artwork->wasChanged(['published_at', 'is_public', 'is_approved', 'deleted_at'])) {
|
||||
$this->bumpExploreCacheVersion();
|
||||
}
|
||||
|
||||
if ($artwork->wasChanged(['published_at', 'is_public', 'is_approved', 'visibility', 'deleted_at', 'published_as_type', 'published_as_id'])) {
|
||||
$this->journeys->requestRebuild((int) $artwork->user_id);
|
||||
}
|
||||
}
|
||||
|
||||
/** Soft delete — remove from search and decrement uploads_count. */
|
||||
@@ -82,12 +104,20 @@ class ArtworkObserver
|
||||
{
|
||||
$this->indexer->delete($artwork->id);
|
||||
$this->userStats->decrementUploads($artwork->user_id);
|
||||
$this->journeys->requestRebuild((int) $artwork->user_id);
|
||||
$this->bumpExploreCacheVersion();
|
||||
|
||||
if ($artwork->features()->exists()) {
|
||||
$this->homepage->clearFeaturedAndMedalCaches();
|
||||
}
|
||||
}
|
||||
|
||||
/** Force delete — ensure removal from index; only decrement if NOT already soft-deleted. */
|
||||
public function forceDeleted(Artwork $artwork): void
|
||||
{
|
||||
$this->indexer->delete($artwork->id);
|
||||
$this->journeys->requestRebuild((int) $artwork->user_id);
|
||||
$this->bumpExploreCacheVersion();
|
||||
|
||||
// If deleted_at was null the artwork was not soft-deleted before;
|
||||
// the deleted() event did NOT fire, so we decrement here.
|
||||
@@ -101,5 +131,25 @@ class ArtworkObserver
|
||||
{
|
||||
$this->indexer->index($artwork);
|
||||
$this->userStats->incrementUploads($artwork->user_id);
|
||||
$this->journeys->requestRebuild((int) $artwork->user_id);
|
||||
$this->bumpExploreCacheVersion();
|
||||
|
||||
if ($artwork->features()->exists()) {
|
||||
$this->homepage->clearFeaturedAndMedalCaches();
|
||||
}
|
||||
}
|
||||
|
||||
private function bumpExploreCacheVersion(): void
|
||||
{
|
||||
Cache::forever('explore.cache.version', ((int) Cache::get('explore.cache.version', 1)) + 1);
|
||||
}
|
||||
|
||||
private function shouldClearFeaturedCaches(Artwork $artwork): bool
|
||||
{
|
||||
if (! $artwork->wasChanged(['published_at', 'is_public', 'is_approved', 'deleted_at', 'has_missing_thumbnails'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $artwork->features()->exists();
|
||||
}
|
||||
}
|
||||
|
||||
37
app/Observers/ContentTypeObserver.php
Normal file
37
app/Observers/ContentTypeObserver.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\ContentType;
|
||||
use App\Models\ContentTypeSlugHistory;
|
||||
use App\Services\ContentTypes\ContentTypeSlugResolver;
|
||||
|
||||
class ContentTypeObserver
|
||||
{
|
||||
public function created(ContentType $contentType): void
|
||||
{
|
||||
app(ContentTypeSlugResolver::class)->flushCaches();
|
||||
}
|
||||
|
||||
public function updated(ContentType $contentType): void
|
||||
{
|
||||
if ($contentType->wasChanged('slug')) {
|
||||
$oldSlug = strtolower(trim((string) $contentType->getOriginal('slug')));
|
||||
$newSlug = strtolower(trim((string) $contentType->slug));
|
||||
|
||||
if ($oldSlug !== '' && $oldSlug !== $newSlug) {
|
||||
ContentTypeSlugHistory::query()->updateOrCreate(
|
||||
['old_slug' => $oldSlug],
|
||||
['content_type_id' => $contentType->id],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
app(ContentTypeSlugResolver::class)->flushCaches();
|
||||
}
|
||||
|
||||
public function deleted(ContentType $contentType): void
|
||||
{
|
||||
app(ContentTypeSlugResolver::class)->flushCaches();
|
||||
}
|
||||
}
|
||||
39
app/Observers/GroupReleaseContributorObserver.php
Normal file
39
app/Observers/GroupReleaseContributorObserver.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\GroupReleaseContributor;
|
||||
use App\Services\Profile\CreatorJourneyService;
|
||||
|
||||
final class GroupReleaseContributorObserver
|
||||
{
|
||||
public function __construct(private readonly CreatorJourneyService $journeys)
|
||||
{
|
||||
}
|
||||
|
||||
public function created(GroupReleaseContributor $contributor): void
|
||||
{
|
||||
$this->journeys->requestRebuild((int) $contributor->user_id);
|
||||
}
|
||||
|
||||
public function updated(GroupReleaseContributor $contributor): void
|
||||
{
|
||||
$this->journeys->requestRebuild((int) $contributor->user_id);
|
||||
|
||||
if ($contributor->wasChanged('user_id')) {
|
||||
$this->journeys->requestRebuild((int) $contributor->getOriginal('user_id'));
|
||||
}
|
||||
}
|
||||
|
||||
public function deleted(GroupReleaseContributor $contributor): void
|
||||
{
|
||||
$this->journeys->requestRebuild((int) $contributor->user_id);
|
||||
}
|
||||
|
||||
public function forceDeleted(GroupReleaseContributor $contributor): void
|
||||
{
|
||||
$this->journeys->requestRebuild((int) $contributor->user_id);
|
||||
}
|
||||
}
|
||||
58
app/Observers/GroupReleaseObserver.php
Normal file
58
app/Observers/GroupReleaseObserver.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\GroupRelease;
|
||||
use App\Services\Profile\CreatorJourneyService;
|
||||
|
||||
final class GroupReleaseObserver
|
||||
{
|
||||
public function __construct(private readonly CreatorJourneyService $journeys)
|
||||
{
|
||||
}
|
||||
|
||||
public function created(GroupRelease $release): void
|
||||
{
|
||||
$this->queueAffectedUsers($release);
|
||||
}
|
||||
|
||||
public function updated(GroupRelease $release): void
|
||||
{
|
||||
if (! $release->wasChanged(['status', 'visibility', 'released_at', 'published_at', 'deleted_at', 'group_id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->queueAffectedUsers($release);
|
||||
}
|
||||
|
||||
public function deleted(GroupRelease $release): void
|
||||
{
|
||||
$this->queueAffectedUsers($release);
|
||||
}
|
||||
|
||||
public function restored(GroupRelease $release): void
|
||||
{
|
||||
$this->queueAffectedUsers($release);
|
||||
}
|
||||
|
||||
public function forceDeleted(GroupRelease $release): void
|
||||
{
|
||||
$this->queueAffectedUsers($release);
|
||||
}
|
||||
|
||||
private function queueAffectedUsers(GroupRelease $release): void
|
||||
{
|
||||
$userIds = $release->contributorLinks()
|
||||
->pluck('user_id')
|
||||
->filter()
|
||||
->map(fn ($userId): int => (int) $userId)
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
foreach ($userIds as $userId) {
|
||||
$this->journeys->requestRebuild((int) $userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user