'array', 'reactions_count' => 'integer', 'comments_count' => 'integer', 'impressions_count' => 'integer', 'saves_count' => 'integer', 'engagement_score' => 'float', 'is_pinned' => 'boolean', 'pinned_order' => 'integer', 'publish_at' => 'datetime', ]; // ───────────────────────────────────────────────────────────────────────── // Constants // ───────────────────────────────────────────────────────────────────────── public const TYPE_TEXT = 'text'; public const TYPE_ARTWORK_SHARE = 'artwork_share'; public const TYPE_UPLOAD = 'upload'; public const TYPE_ACHIEVEMENT = 'achievement'; public const VISIBILITY_PUBLIC = 'public'; public const VISIBILITY_FOLLOWERS = 'followers'; public const VISIBILITY_PRIVATE = 'private'; public const STATUS_DRAFT = 'draft'; public const STATUS_SCHEDULED = 'scheduled'; public const STATUS_PUBLISHED = 'published'; // ───────────────────────────────────────────────────────────────────────── // Relationships // ───────────────────────────────────────────────────────────────────────── public function user(): BelongsTo { return $this->belongsTo(User::class); } public function targets(): HasMany { return $this->hasMany(PostTarget::class); } /** Convenience: single artwork target for artwork_share posts */ public function artworkTarget(): HasOne { return $this->hasOne(PostTarget::class)->where('target_type', 'artwork'); } public function reactions(): HasMany { return $this->hasMany(PostReaction::class); } public function comments(): HasMany { return $this->hasMany(PostComment::class)->orderBy('created_at'); } public function saves(): HasMany { return $this->hasMany(PostSave::class); } public function reports(): HasMany { return $this->hasMany(PostReport::class); } public function hashtags(): HasMany { return $this->hasMany(PostHashtag::class); } // ───────────────────────────────────────────────────────────────────────── // Scopes // ───────────────────────────────────────────────────────────────────────── /** Posts visible to any public (non-authenticated) visitor */ public function scopePublic($query) { return $query->where('visibility', self::VISIBILITY_PUBLIC); } /** Only published posts */ public function scopePublished($query) { return $query->where('status', self::STATUS_PUBLISHED); } /** Only scheduled posts */ public function scopeScheduled($query) { return $query->where('status', self::STATUS_SCHEDULED); } /** Posts visible to the given viewer (respects followers-only AND published status) */ public function scopeVisibleTo($query, ?int $viewerId) { $query->where('status', self::STATUS_PUBLISHED); if (! $viewerId) { return $query->where('visibility', self::VISIBILITY_PUBLIC); } return $query->where(function ($q) use ($viewerId) { $q->where('visibility', self::VISIBILITY_PUBLIC) ->orWhere('user_id', $viewerId) ->orWhere(function ($q2) use ($viewerId) { $q2->where('visibility', self::VISIBILITY_FOLLOWERS) ->whereIn('user_id', function ($sub) use ($viewerId) { $sub->select('user_id') ->from('user_followers') ->where('follower_id', $viewerId); }); }); }); } // ───────────────────────────────────────────────────────────────────────── // Scout (Meilisearch) // ───────────────────────────────────────────────────────────────────────── public function toSearchableArray(): array { return [ 'id' => $this->id, 'body' => strip_tags($this->body ?? ''), 'hashtags' => $this->hashtags->pluck('tag')->toArray(), 'user_id' => $this->user_id, 'type' => $this->type, 'visibility' => $this->visibility, 'created_at' => $this->created_at?->timestamp, ]; } public function shouldBeSearchable(): bool { return $this->status === self::STATUS_PUBLISHED && $this->visibility === self::VISIBILITY_PUBLIC; } }