messages implemented

This commit is contained in:
2026-02-26 21:12:32 +01:00
parent d0aefc5ddc
commit 15b7b77d20
168 changed files with 14728 additions and 6786 deletions

View File

@@ -0,0 +1,32 @@
<?php
namespace Database\Factories;
use App\Models\Artwork;
use App\Models\ArtworkComment;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class ArtworkCommentFactory extends Factory
{
protected $model = ArtworkComment::class;
public function definition(): array
{
$raw = $this->faker->sentence(12);
return [
'artwork_id' => Artwork::factory(),
'user_id' => User::factory(),
'content' => $raw,
'raw_content' => $raw,
'rendered_content' => '<p>' . e($raw) . '</p>',
'is_approved' => true,
];
}
public function unapproved(): static
{
return $this->state(['is_approved' => false]);
}
}

View File

@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('artwork_comments', function (Blueprint $table) {
// raw_content stores user-submitted markdown/plain text
// rendered_content stores the sanitized HTML cache
$table->mediumText('raw_content')->nullable()->after('content');
$table->mediumText('rendered_content')->nullable()->after('raw_content');
});
}
public function down(): void
{
Schema::table('artwork_comments', function (Blueprint $table) {
$table->dropColumn(['raw_content', 'rendered_content']);
});
}
};

View File

@@ -0,0 +1,53 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Allowed reaction slugs emoji mapping (authoritative list lives in ReactionType).
* Stored as VARCHAR to avoid MySQL ENUM emoji encoding issues.
*/
public function up(): void
{
Schema::create('artwork_reactions', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('artwork_id');
$table->unsignedBigInteger('user_id');
// slug: thumbs_up | heart | fire | laugh | clap | wow
$table->string('reaction', 20);
$table->timestamp('created_at')->useCurrent();
$table->unique(['artwork_id', 'user_id', 'reaction'], 'artwork_reactions_unique');
$table->index('artwork_id');
$table->index('user_id');
$table->foreign('artwork_id')->references('id')->on('artworks')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
Schema::create('comment_reactions', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('comment_id');
$table->unsignedBigInteger('user_id');
// slug: thumbs_up | heart | fire | laugh | clap | wow
$table->string('reaction', 20);
$table->timestamp('created_at')->useCurrent();
$table->unique(['comment_id', 'user_id', 'reaction'], 'comment_reactions_unique');
$table->index('comment_id');
$table->index('user_id');
$table->foreign('comment_id')->references('id')->on('artwork_comments')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
}
public function down(): void
{
Schema::dropIfExists('comment_reactions');
Schema::dropIfExists('artwork_reactions');
}
};

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Upgrade legacy TEXT columns to MEDIUMTEXT so large imported comments
* (incl. spam-heavy legacy rows) do not cause truncation errors.
*
* TEXT = 65,535 bytes (~64 KB)
* MEDIUMTEXT = 16,777,215 bytes (~16 MB)
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('artwork_comments', function (Blueprint $table) {
$table->mediumText('content')->nullable()->change();
});
Schema::table('forum_posts', function (Blueprint $table) {
$table->mediumText('content')->nullable()->change();
});
}
public function down(): void
{
Schema::table('artwork_comments', function (Blueprint $table) {
$table->text('content')->nullable()->change();
});
Schema::table('forum_posts', function (Blueprint $table) {
$table->text('content')->nullable()->change();
});
}
};

View File

@@ -0,0 +1,56 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Modernized replacement for the legacy `favourites` table.
*
* What changed from the legacy schema:
* - Renamed table: favourites artwork_favourites
* - `favourite_id` `id` (bigint unsigned, auto-increment)
* - `datum` `created_at` / `updated_at` via timestamps()
* - `user_type` dropped membership tier is not a property of the
* favourite relationship; query via users.role if needed
* - `author_id` dropped always derivable via artworks.user_id
* - Both FKs are constrained with cascadeOnDelete so orphaned rows are
* automatically cleaned up when an artwork or user is hard-deleted
* - `legacy_id` tracks the original favourite_id for idempotent re-imports
*/
return new class extends Migration
{
public function up(): void
{
Schema::create('artwork_favourites', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')
->constrained('users')
->cascadeOnDelete();
$table->foreignId('artwork_id')
->constrained('artworks')
->cascadeOnDelete();
// Preserve original legacy PK for idempotent re-imports.
// NULL for favourites created natively in the new system.
$table->unsignedInteger('legacy_id')->nullable()->unique();
$table->timestamps();
// Prevent duplicate favourites
$table->unique(['user_id', 'artwork_id'], 'artwork_favourites_unique_user_artwork');
// Fast lookup: "how many favourites does this artwork have?"
$table->index('artwork_id');
// Fast lookup: "which artworks has this user favourited?"
$table->index('user_id');
});
}
public function down(): void
{
Schema::dropIfExists('artwork_favourites');
}
};

View File

@@ -0,0 +1,53 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* Consolidate onto artwork_favourites.
*
* Any rows in the interim user_favorites table (created 2026-02-07) that are
* not already present in artwork_favourites are copied over, then
* user_favorites is dropped so only one favourites table remains.
*/
return new class extends Migration
{
public function up(): void
{
if (! Schema::hasTable('user_favorites')) {
return;
}
// Copy any rows not yet in artwork_favourites, using insertOrIgnore so the
// unique (user_id, artwork_id) constraint silently skips duplicates.
// chunk() avoids memory spikes on large tables.
DB::table('user_favorites')->orderBy('id')->chunk(500, function ($rows) {
DB::table('artwork_favourites')->insertOrIgnore(
$rows->map(fn ($r) => [
'user_id' => $r->user_id,
'artwork_id' => $r->artwork_id,
'created_at' => $r->created_at,
'updated_at' => $r->created_at,
])->all()
);
});
Schema::drop('user_favorites');
}
public function down(): void
{
if (Schema::hasTable('user_favorites')) {
return;
}
Schema::create('user_favorites', function ($table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->unsignedBigInteger('artwork_id');
$table->timestamp('created_at')->nullable();
$table->unique(['user_id', 'artwork_id']);
});
}
};

View File

@@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('user_statistics', function (Blueprint $table) {
$table->unsignedInteger('followers_count')->default(0)->after('profile_views');
$table->unsignedInteger('following_count')->default(0)->after('followers_count');
});
// Backfill follow counters using subquery syntax (compatible with MySQL + SQLite).
DB::statement("
UPDATE user_statistics
SET followers_count = (
SELECT COUNT(*) FROM user_followers
WHERE user_followers.user_id = user_statistics.user_id
)
");
DB::statement("
UPDATE user_statistics
SET following_count = (
SELECT COUNT(*) FROM user_followers
WHERE user_followers.follower_id = user_statistics.user_id
)
");
}
public function down(): void
{
Schema::table('user_statistics', function (Blueprint $table) {
$table->dropColumn(['followers_count', 'following_count']);
});
}
};

View File

@@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* User Statistics v2 in-place schema upgrade.
*
* Renames legacy columns, widens all counters to unsignedBigInteger,
* and adds creator-received metrics + activity timestamps.
*
* Mapping:
* uploads uploads_count
* downloads downloads_received_count
* pageviews artwork_views_received_count
* awards awards_received_count
* profile_views profile_views_count
*
* New columns added:
* favorites_received_count, comments_received_count,
* reactions_received_count, last_upload_at, last_active_at
*
* followers_count / following_count already exist with correct naming only
* widened to bigint.
*/
return new class extends Migration
{
public function up(): void
{
// ── 1. Rename legacy columns ─────────────────────────────────────────
Schema::table('user_statistics', function (Blueprint $table) {
if (Schema::hasColumn('user_statistics', 'uploads')) {
$table->renameColumn('uploads', 'uploads_count');
}
if (Schema::hasColumn('user_statistics', 'downloads')) {
$table->renameColumn('downloads', 'downloads_received_count');
}
if (Schema::hasColumn('user_statistics', 'pageviews')) {
$table->renameColumn('pageviews', 'artwork_views_received_count');
}
if (Schema::hasColumn('user_statistics', 'awards')) {
$table->renameColumn('awards', 'awards_received_count');
}
if (Schema::hasColumn('user_statistics', 'profile_views')) {
$table->renameColumn('profile_views', 'profile_views_count');
}
});
// ── 2. Widen to unsignedBigInteger + add new columns ─────────────────
Schema::table('user_statistics', function (Blueprint $table) {
// Widen existing counters
$table->unsignedBigInteger('uploads_count')->default(0)->change();
$table->unsignedBigInteger('downloads_received_count')->default(0)->change();
$table->unsignedBigInteger('artwork_views_received_count')->default(0)->change();
$table->unsignedBigInteger('awards_received_count')->default(0)->change();
$table->unsignedBigInteger('profile_views_count')->default(0)->change();
$table->unsignedBigInteger('followers_count')->default(0)->change();
$table->unsignedBigInteger('following_count')->default(0)->change();
// Add new creator-received counters
if (! Schema::hasColumn('user_statistics', 'favorites_received_count')) {
$table->unsignedBigInteger('favorites_received_count')->default(0)->after('awards_received_count');
}
if (! Schema::hasColumn('user_statistics', 'comments_received_count')) {
$table->unsignedBigInteger('comments_received_count')->default(0)->after('favorites_received_count');
}
if (! Schema::hasColumn('user_statistics', 'reactions_received_count')) {
$table->unsignedBigInteger('reactions_received_count')->default(0)->after('comments_received_count');
}
// Activity timestamps
if (! Schema::hasColumn('user_statistics', 'last_upload_at')) {
$table->timestamp('last_upload_at')->nullable()->after('reactions_received_count');
}
if (! Schema::hasColumn('user_statistics', 'last_active_at')) {
$table->timestamp('last_active_at')->nullable()->after('last_upload_at');
}
});
// ── 3. Optional: indexes for creator ranking ─────────────────────────
try {
Schema::table('user_statistics', function (Blueprint $table) {
$table->index('awards_received_count', 'idx_us_awards');
});
} catch (\Throwable) {}
try {
Schema::table('user_statistics', function (Blueprint $table) {
$table->index('favorites_received_count', 'idx_us_favorites');
});
} catch (\Throwable) {}
}
public function down(): void
{
// Remove added columns
Schema::table('user_statistics', function (Blueprint $table) {
foreach (['favorites_received_count', 'comments_received_count', 'reactions_received_count', 'last_upload_at', 'last_active_at'] as $col) {
if (Schema::hasColumn('user_statistics', $col)) {
$table->dropColumn($col);
}
}
});
// Drop indexes
Schema::table('user_statistics', function (Blueprint $table) {
try { $table->dropIndex('idx_us_awards'); } catch (\Throwable) {}
try { $table->dropIndex('idx_us_favorites'); } catch (\Throwable) {}
});
// Rename back
Schema::table('user_statistics', function (Blueprint $table) {
if (Schema::hasColumn('user_statistics', 'uploads_count')) {
$table->renameColumn('uploads_count', 'uploads');
}
if (Schema::hasColumn('user_statistics', 'downloads_received_count')) {
$table->renameColumn('downloads_received_count', 'downloads');
}
if (Schema::hasColumn('user_statistics', 'artwork_views_received_count')) {
$table->renameColumn('artwork_views_received_count', 'pageviews');
}
if (Schema::hasColumn('user_statistics', 'awards_received_count')) {
$table->renameColumn('awards_received_count', 'awards');
}
if (Schema::hasColumn('user_statistics', 'profile_views_count')) {
$table->renameColumn('profile_views_count', 'profile_views');
}
});
}
};

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('conversations', function (Blueprint $table) {
$table->id();
$table->enum('type', ['direct', 'group'])->default('direct');
$table->string('title')->nullable();
$table->unsignedBigInteger('created_by');
$table->timestamp('last_message_at')->nullable()->index();
$table->timestamps();
$table->foreign('created_by')->references('id')->on('users')->onDelete('cascade');
});
}
public function down(): void
{
Schema::dropIfExists('conversations');
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('conversation_participants', function (Blueprint $table) {
$table->id();
$table->foreignId('conversation_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->enum('role', ['member', 'admin'])->default('member');
$table->timestamp('last_read_at')->nullable();
$table->boolean('is_muted')->default(false);
$table->boolean('is_archived')->default(false);
$table->timestamp('joined_at')->useCurrent();
$table->timestamp('left_at')->nullable();
$table->unique(['conversation_id', 'user_id']);
$table->index('user_id');
$table->index('conversation_id');
});
}
public function down(): void
{
Schema::dropIfExists('conversation_participants');
}
};

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->foreignId('conversation_id')->constrained()->onDelete('cascade');
$table->foreignId('sender_id')->references('id')->on('users')->onDelete('cascade');
$table->mediumText('body');
$table->timestamp('edited_at')->nullable();
$table->softDeletes();
$table->timestamps();
$table->index(['conversation_id', 'created_at']);
$table->index('sender_id');
});
}
public function down(): void
{
Schema::dropIfExists('messages');
}
};

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('message_reactions', function (Blueprint $table) {
$table->id();
$table->foreignId('message_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('reaction', 32);
$table->timestamp('created_at')->useCurrent();
$table->unique(['message_id', 'user_id', 'reaction']);
$table->index('message_id');
});
}
public function down(): void
{
Schema::dropIfExists('message_reactions');
}
};

View File

@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->enum('allow_messages_from', ['everyone', 'followers', 'mutual_followers', 'nobody'])
->default('everyone')
->after('role');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('allow_messages_from');
});
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (Schema::hasTable('notifications')) {
return;
}
Schema::create('notifications', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('type', 32);
$table->json('data');
$table->timestamp('read_at')->nullable();
$table->timestamps();
$table->index('user_id');
$table->index('read_at');
});
}
public function down(): void
{
Schema::dropIfExists('notifications');
}
};

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('conversation_participants', function (Blueprint $table): void {
if (! Schema::hasColumn('conversation_participants', 'is_pinned')) {
$table->boolean('is_pinned')->default(false)->after('is_archived');
}
if (! Schema::hasColumn('conversation_participants', 'pinned_at')) {
$table->timestamp('pinned_at')->nullable()->after('is_pinned');
}
$table->index(['user_id', 'is_pinned']);
});
}
public function down(): void
{
Schema::table('conversation_participants', function (Blueprint $table): void {
if (Schema::hasColumn('conversation_participants', 'pinned_at')) {
$table->dropColumn('pinned_at');
}
if (Schema::hasColumn('conversation_participants', 'is_pinned')) {
$table->dropColumn('is_pinned');
}
});
}
};

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('message_attachments', function (Blueprint $table): void {
$table->id();
$table->foreignId('message_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->enum('type', ['image', 'file']);
$table->string('mime', 191);
$table->unsignedBigInteger('size_bytes');
$table->unsignedInteger('width')->nullable();
$table->unsignedInteger('height')->nullable();
$table->string('sha256', 64)->nullable();
$table->string('original_name', 255);
$table->string('storage_path', 500);
$table->timestamp('created_at')->useCurrent();
$table->index('message_id');
$table->index('user_id');
});
}
public function down(): void
{
Schema::dropIfExists('message_attachments');
}
};

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (Schema::hasTable('reports')) {
return;
}
Schema::create('reports', function (Blueprint $table): void {
$table->id();
$table->foreignId('reporter_id')->constrained('users')->cascadeOnDelete();
$table->enum('target_type', ['message', 'conversation', 'user']);
$table->unsignedBigInteger('target_id');
$table->string('reason', 120);
$table->text('details')->nullable();
$table->enum('status', ['open', 'reviewing', 'closed'])->default('open');
$table->timestamps();
$table->index(['target_type', 'target_id']);
$table->index('status');
$table->index('reporter_id');
});
}
public function down(): void
{
Schema::dropIfExists('reports');
}
};

View File

@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('message_reactions', function (Blueprint $table): void {
$table->index('user_id');
});
}
public function down(): void
{
Schema::table('message_reactions', function (Blueprint $table): void {
$table->dropIndex(['user_id']);
});
}
};