Repair: copy legacy joinDate into new user's created_at when creating users from legacy wallz

This commit is contained in:
2026-03-22 09:13:39 +01:00
parent e8b5edf5d2
commit 2608be7420
80 changed files with 3991 additions and 723 deletions

View File

@@ -197,7 +197,7 @@ class ArtworkCommentController extends Controller
'id' => $c->id,
'parent_id' => $c->parent_id,
'raw_content' => $c->raw_content ?? $c->content,
'rendered_content' => $c->rendered_content ?? e(strip_tags($c->content ?? '')),
'rendered_content' => $this->renderCommentContent($c),
'created_at' => $c->created_at?->toIso8601String(),
'time_ago' => $c->created_at ? Carbon::parse($c->created_at)->diffForHumans() : null,
'can_edit' => $currentUserId === $userId,
@@ -224,6 +224,31 @@ class ArtworkCommentController extends Controller
return $data;
}
private function renderCommentContent(ArtworkComment $comment): string
{
$rawContent = (string) ($comment->raw_content ?? $comment->content ?? '');
$renderedContent = $comment->rendered_content;
if (! is_string($renderedContent) || trim($renderedContent) === '') {
$renderedContent = $rawContent !== ''
? ContentSanitizer::render($rawContent)
: nl2br(e(strip_tags((string) ($comment->content ?? ''))));
}
return ContentSanitizer::sanitizeRenderedHtml(
$renderedContent,
$this->commentAuthorCanPublishLinks($comment)
);
}
private function commentAuthorCanPublishLinks(ArtworkComment $comment): bool
{
$level = (int) ($comment->user?->level ?? 1);
$rank = strtolower((string) ($comment->user?->rank ?? 'Newbie'));
return $level > 1 && $rank !== 'newbie';
}
private function notifyRecipients(Artwork $artwork, ArtworkComment $comment, User $actor, ?int $parentId): void
{
$notifiedUserIds = [];

View File

@@ -9,10 +9,11 @@ use App\Http\Requests\Messaging\RenameConversationRequest;
use App\Http\Requests\Messaging\StoreConversationRequest;
use App\Models\Conversation;
use App\Models\ConversationParticipant;
use App\Models\Message;
use App\Models\User;
use App\Services\Messaging\ConversationReadService;
use App\Services\Messaging\ConversationStateService;
use App\Services\Messaging\SendMessageAction;
use App\Services\Messaging\UnreadCounterService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
@@ -23,7 +24,9 @@ class ConversationController extends Controller
{
public function __construct(
private readonly ConversationStateService $conversationState,
private readonly ConversationReadService $conversationReads,
private readonly SendMessageAction $sendMessage,
private readonly UnreadCounterService $unreadCounters,
) {}
// ── GET /api/messages/conversations ─────────────────────────────────────
@@ -36,26 +39,13 @@ class ConversationController extends Controller
$cacheKey = $this->conversationListCacheKey($user->id, $page, $cacheVersion);
$conversations = Cache::remember($cacheKey, now()->addSeconds(20), function () use ($user, $page) {
return Conversation::query()
$query = Conversation::query()
->select('conversations.*')
->join('conversation_participants as cp_me', function ($join) use ($user) {
$join->on('cp_me.conversation_id', '=', 'conversations.id')
->where('cp_me.user_id', '=', $user->id)
->whereNull('cp_me.left_at');
})
->addSelect([
'unread_count' => Message::query()
->selectRaw('count(*)')
->whereColumn('messages.conversation_id', 'conversations.id')
->where('messages.sender_id', '!=', $user->id)
->whereNull('messages.deleted_at')
->where(function ($query) {
$query->whereNull('cp_me.last_read_message_id')
->whereNull('cp_me.last_read_at')
->orWhereColumn('messages.id', '>', 'cp_me.last_read_message_id')
->orWhereColumn('messages.created_at', '>', 'cp_me.last_read_at');
}),
])
->where('conversations.is_active', true)
->with([
'allParticipants' => fn ($q) => $q->whereNull('left_at')->with(['user:id,username']),
@@ -64,8 +54,11 @@ class ConversationController extends Controller
->orderByDesc('cp_me.is_pinned')
->orderByDesc('cp_me.pinned_at')
->orderByDesc('last_message_at')
->orderByDesc('conversations.id')
->paginate(20, ['conversations.*'], 'page', $page);
->orderByDesc('conversations.id');
$this->unreadCounters->applyUnreadCountSelect($query, $user, 'cp_me');
return $query->paginate(20, ['conversations.*'], 'page', $page);
});
$conversations->through(function ($conv) use ($user) {
@@ -74,7 +67,12 @@ class ConversationController extends Controller
return $conv;
});
return response()->json($conversations);
return response()->json([
...$conversations->toArray(),
'summary' => [
'unread_total' => $this->unreadCounters->totalUnreadForUser($user),
],
]);
}
// ── GET /api/messages/conversation/{id} ─────────────────────────────────
@@ -110,7 +108,7 @@ class ConversationController extends Controller
public function markRead(Request $request, int $id): JsonResponse
{
$conversation = $this->findAuthorized($request, $id);
$participant = $this->conversationState->markConversationRead(
$participant = $this->conversationReads->markConversationRead(
$conversation,
$request->user(),
$request->integer('message_id') ?: null,
@@ -120,6 +118,7 @@ class ConversationController extends Controller
'ok' => true,
'last_read_at' => optional($participant->last_read_at)?->toIso8601String(),
'last_read_message_id' => $participant->last_read_message_id,
'unread_total' => $this->unreadCounters->totalUnreadForUser($request->user()),
]);
}

View File

@@ -13,6 +13,7 @@ use App\Models\Conversation;
use App\Models\ConversationParticipant;
use App\Models\Message;
use App\Models\MessageReaction;
use App\Services\Messaging\ConversationDeltaService;
use App\Services\Messaging\ConversationStateService;
use App\Services\Messaging\MessagingPayloadFactory;
use App\Services\Messaging\MessageSearchIndexer;
@@ -26,6 +27,7 @@ class MessageController extends Controller
private const PAGE_SIZE = 30;
public function __construct(
private readonly ConversationDeltaService $conversationDelta,
private readonly ConversationStateService $conversationState,
private readonly MessagingPayloadFactory $payloadFactory,
private readonly SendMessageAction $sendMessage,
@@ -40,15 +42,7 @@ class MessageController extends Controller
$afterId = $request->integer('after_id');
if ($afterId) {
$messages = Message::withTrashed()
->where('conversation_id', $conversationId)
->with(['sender:id,username', 'reactions', 'attachments'])
->where('id', '>', $afterId)
->orderBy('id')
->limit(100)
->get()
->map(fn (Message $message) => $this->payloadFactory->message($message, (int) $request->user()->id))
->values();
$messages = $this->conversationDelta->messagesAfter($conversation, $request->user(), $afterId);
return response()->json([
'data' => $messages,
@@ -77,6 +71,18 @@ class MessageController extends Controller
]);
}
public function delta(Request $request, int $conversationId): JsonResponse
{
$conversation = $this->findConversationOrFail($conversationId);
$afterMessageId = max(0, (int) $request->integer('after_message_id'));
abort_if($afterMessageId < 1, 422, 'after_message_id is required.');
return response()->json([
'data' => $this->conversationDelta->messagesAfter($conversation, $request->user(), $afterMessageId),
]);
}
// ── POST /api/messages/{conversation_id} ─────────────────────────────────
public function store(StoreMessageRequest $request, int $conversationId): JsonResponse

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers\Api\Messaging;
use App\Http\Controllers\Controller;
use App\Models\Conversation;
use App\Services\Messaging\MessagingPresenceService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class PresenceController extends Controller
{
public function __construct(
private readonly MessagingPresenceService $presence,
) {}
public function heartbeat(Request $request): JsonResponse
{
$conversationId = $request->integer('conversation_id') ?: null;
if ($conversationId) {
$conversation = Conversation::query()->findOrFail($conversationId);
$this->authorize('view', $conversation);
}
$this->presence->touch($request->user(), $conversationId);
return response()->json([
'ok' => true,
'conversation_id' => $conversationId,
]);
}
}

View File

@@ -37,7 +37,14 @@ final class ProfileApiController extends Controller
$isOwner = Auth::check() && Auth::id() === $user->id;
$sort = $request->input('sort', 'latest');
$query = Artwork::with('user:id,name,username')
$query = Artwork::with([
'user:id,name,username,level,rank',
'stats:artwork_id,views,downloads,favorites',
'categories' => function ($query) {
$query->select('categories.id', 'categories.content_type_id', 'categories.parent_id', 'categories.name', 'categories.slug', 'categories.sort_order')
->with(['contentType:id,slug,name']);
},
])
->where('user_id', $user->id)
->whereNull('deleted_at');
@@ -106,7 +113,14 @@ final class ProfileApiController extends Controller
return response()->json(['data' => [], 'next_cursor' => null, 'has_more' => false]);
}
$indexed = Artwork::with('user:id,name,username')
$indexed = Artwork::with([
'user:id,name,username,level,rank',
'stats:artwork_id,views,downloads,favorites',
'categories' => function ($query) {
$query->select('categories.id', 'categories.content_type_id', 'categories.parent_id', 'categories.name', 'categories.slug', 'categories.sort_order')
->with(['contentType:id,slug,name']);
},
])
->whereIn('id', $favIds)
->get()
->keyBy('id');
@@ -173,6 +187,9 @@ final class ProfileApiController extends Controller
private function mapArtworkCardPayload(Artwork $art): array
{
$present = ThumbnailPresenter::present($art, 'md');
$category = $art->categories->first();
$contentType = $category?->contentType;
$stats = $art->stats;
return [
'id' => $art->id,
@@ -183,6 +200,13 @@ final class ProfileApiController extends Controller
'height' => $art->height,
'username' => $art->user->username ?? null,
'uname' => $art->user->username ?? $art->user->name ?? 'Skinbase',
'content_type' => $contentType?->name,
'content_type_slug' => $contentType?->slug,
'category' => $category?->name,
'category_slug' => $category?->slug,
'views' => (int) ($stats?->views ?? $art->view_count ?? 0),
'downloads' => (int) ($stats?->downloads ?? 0),
'likes' => (int) ($stats?->favorites ?? $art->favourite_count ?? 0),
'published_at' => $this->formatIsoDate($art->published_at),
];
}