diff --git a/app/Actions/Article/CreateArticleAction.php b/app/Actions/Article/CreateArticleAction.php index a960ba73..824e1528 100644 --- a/app/Actions/Article/CreateArticleAction.php +++ b/app/Actions/Article/CreateArticleAction.php @@ -9,14 +9,13 @@ use App\Models\Article; use App\Notifications\PostArticleToTelegram; use Carbon\Carbon; -use DateTimeInterface; use Illuminate\Support\Facades\Auth; final class CreateArticleAction { public function execute(CreateArticleData $articleData): Article { - if ($articleData->publishedAt && ! ($articleData->publishedAt instanceof DateTimeInterface)) { + if ($articleData->publishedAt) { $articleData->publishedAt = new Carbon( time: $articleData->publishedAt, tz: config('app.timezone') @@ -41,12 +40,14 @@ public function execute(CreateArticleData $articleData): Article } if ($articleData->file) { - $article->addMedia($articleData->file->getRealPath())->toMediaCollection('media'); + $article->addMedia($articleData->file->getRealPath()) + ->toMediaCollection('media'); } if ($article->isAwaitingApproval()) { // Envoi de la notification sur le channel Telegram pour la validation de l'article. Auth::user()?->notify(new PostArticleToTelegram($article)); + session()->flash('status', __('notifications.article.created')); } diff --git a/app/Actions/Forum/CreateReplyAction.php b/app/Actions/Forum/CreateReplyAction.php new file mode 100644 index 00000000..ee55249a --- /dev/null +++ b/app/Actions/Forum/CreateReplyAction.php @@ -0,0 +1,29 @@ + $body]); + $reply->authoredBy($user); + $reply->to($model); + $reply->save(); + + givePoint(new ReplyCreated($model, $user)); + event(new ReplyWasCreated($reply)); + } +} diff --git a/app/Actions/Forum/SubscribeToThreadAction.php b/app/Actions/Forum/SubscribeToThreadAction.php new file mode 100644 index 00000000..73bc91de --- /dev/null +++ b/app/Actions/Forum/SubscribeToThreadAction.php @@ -0,0 +1,23 @@ +uuid = Uuid::uuid4()->toString(); + $subscription->user()->associate(Auth::user()); + $subscription->subscribeAble()->associate($thread); + + $thread->subscribes()->save($subscription); + } +} diff --git a/app/Console/Commands/UpdateUserBestRepliesPoints.php b/app/Console/Commands/UpdateUserBestRepliesPoints.php index 7fc30bf3..afc4f842 100644 --- a/app/Console/Commands/UpdateUserBestRepliesPoints.php +++ b/app/Console/Commands/UpdateUserBestRepliesPoints.php @@ -7,6 +7,7 @@ use App\Gamify\Points\BestReply; use App\Models\Thread; use Illuminate\Console\Command; +use Illuminate\Support\Collection; final class UpdateUserBestRepliesPoints extends Command { @@ -18,10 +19,11 @@ public function handle(): void { $this->info('Updating users bests replies reputations...'); - $resolvedThread = Thread::resolved()->with('solutionReply')->get(); + /** @var Collection | Thread[] $resolvedThread */ + $resolvedThread = Thread::with('solutionReply')->scopes('resolved')->get(); foreach ($resolvedThread as $thread) { - givePoint(new BestReply($thread->solutionReply)); + givePoint(new BestReply($thread->solutionReply)); // @phpstan-ignore-line } $this->info('All done!'); diff --git a/app/Exceptions/UnverifiedUserException.php b/app/Exceptions/UnverifiedUserException.php new file mode 100644 index 00000000..53dc7164 --- /dev/null +++ b/app/Exceptions/UnverifiedUserException.php @@ -0,0 +1,9 @@ +subject = $subject; } - - public function payee(): User - { - // @phpstan-ignore-next-line - return $this->getSubject()->user; - } } diff --git a/app/Gamify/Points/BestReply.php b/app/Gamify/Points/BestReply.php index e7b41703..cd643c43 100644 --- a/app/Gamify/Points/BestReply.php +++ b/app/Gamify/Points/BestReply.php @@ -4,15 +4,16 @@ namespace App\Gamify\Points; +use App\Models\Reply; use QCod\Gamify\PointType; final class BestReply extends PointType { public int $points = 20; - protected string $payee = 'author'; + protected string $payee = 'user'; - public function __construct(mixed $subject) + public function __construct(Reply $subject) { $this->subject = $subject; } diff --git a/app/Gamify/Points/DiscussionCreated.php b/app/Gamify/Points/DiscussionCreated.php index 04083e9c..66e94afc 100644 --- a/app/Gamify/Points/DiscussionCreated.php +++ b/app/Gamify/Points/DiscussionCreated.php @@ -5,21 +5,16 @@ namespace App\Gamify\Points; use App\Models\Discussion; -use App\Models\User; use QCod\Gamify\PointType; final class DiscussionCreated extends PointType { public int $points = 20; + protected string $payee = 'user'; + public function __construct(Discussion $subject) { $this->subject = $subject; } - - public function payee(): User - { - // @phpstan-ignore-next-line - return $this->getSubject()->user; - } } diff --git a/app/Gamify/Points/PostCreated.php b/app/Gamify/Points/PostCreated.php index 950a7180..5ec37b35 100644 --- a/app/Gamify/Points/PostCreated.php +++ b/app/Gamify/Points/PostCreated.php @@ -5,21 +5,16 @@ namespace App\Gamify\Points; use App\Models\Article; -use App\Models\User; use QCod\Gamify\PointType; final class PostCreated extends PointType { public int $points = 50; + protected string $payee = 'user'; + public function __construct(Article $subject) { $this->subject = $subject; } - - public function payee(): User - { - // @phpstan-ignore-next-line - return $this->getSubject()->user; - } } diff --git a/app/Gamify/Points/ThreadCreated.php b/app/Gamify/Points/ThreadCreated.php index 9cbf930b..d84f0204 100644 --- a/app/Gamify/Points/ThreadCreated.php +++ b/app/Gamify/Points/ThreadCreated.php @@ -5,21 +5,16 @@ namespace App\Gamify\Points; use App\Models\Thread; -use App\Models\User; use QCod\Gamify\PointType; final class ThreadCreated extends PointType { public int $points = 55; + protected string $payee = 'user'; + public function __construct(Thread $subject) { $this->subject = $subject; } - - public function payee(): User - { - // @phpstan-ignore-next-line - return $this->getSubject()->user; - } } diff --git a/app/Http/Controllers/ThreadController.php b/app/Http/Controllers/ThreadController.php deleted file mode 100644 index 7011e5c2..00000000 --- a/app/Http/Controllers/ThreadController.php +++ /dev/null @@ -1,62 +0,0 @@ -middleware(['auth', 'verified'], ['only' => ['create', 'edit']]); - } - - public function index(Request $request): View - { - $filter = getFilter('sortBy', ['recent', 'resolved', 'unresolved']); - $threads = Thread::filter($request) - ->withviewscount() - ->orderByDesc('created_at') - ->paginate(10); - - return view('forum.index', [ - 'channel' => null, - 'threads' => $threads, - 'filter' => $filter, - ]); - } - - public function channel(Request $request, Channel $channel): View - { - $filter = getFilter('sortBy', ['recent', 'resolved', 'unresolved']); - $threads = Thread::forChannel($channel) - ->filter($request) - ->withviewscount() - ->orderByDesc('created_at') - ->paginate(10); - - return view('forum.index', compact('channel', 'threads', 'filter')); - } - - public function create(): View - { - return view('forum.create'); - } - - public function show(Thread $thread): View - { - views($thread)->record(); - - return view('forum.thread', compact('thread')); - } - - public function edit(Thread $thread): View - { - return view('forum.edit', compact('thread')); - } -} diff --git a/app/Listeners/SendNewCommentNotification.php b/app/Listeners/SendNewCommentNotification.php index d125e08b..48d6a636 100644 --- a/app/Listeners/SendNewCommentNotification.php +++ b/app/Listeners/SendNewCommentNotification.php @@ -17,9 +17,7 @@ public function handle(CommentWasAdded $event): void foreach ($discussion->subscribes as $subscription) { /** @var Subscribe $subscription */ - // @phpstan-ignore-next-line if ($this->replyAuthorDoesNotMatchSubscriber(author: $event->reply->user, subscription: $subscription)) { - // @phpstan-ignore-next-line $subscription->user->notify(new NewCommentNotification( reply: $event->reply, subscription: $subscription, diff --git a/app/Listeners/SendNewReplyNotification.php b/app/Listeners/SendNewReplyNotification.php index be55d1de..bf627b2f 100644 --- a/app/Listeners/SendNewReplyNotification.php +++ b/app/Listeners/SendNewReplyNotification.php @@ -22,9 +22,8 @@ public function handle(ReplyWasCreated $event): void foreach ($thread->subscribes as $subscription) { /** @var Subscribe $subscription */ - // @phpstan-ignore-next-line if ($this->replyAuthorDoesNotMatchSubscriber(author: $event->reply->user, subscription: $subscription)) { - $subscription->user->notify(new NewReplyNotification($event->reply, $subscription)); // @phpstan-ignore-line + $subscription->user->notify(new NewReplyNotification($event->reply, $subscription)); } } } diff --git a/app/Listeners/SendNewThreadNotification.php b/app/Listeners/SendNewThreadNotification.php index 80b02e65..ecc1b9b1 100644 --- a/app/Listeners/SendNewThreadNotification.php +++ b/app/Listeners/SendNewThreadNotification.php @@ -13,6 +13,6 @@ public function handle(ThreadWasCreated $event): void { $thread = $event->thread; - $thread->user->notify(new PostThreadToSlack($thread)); // @phpstan-ignore-line + $thread->user->notify(new PostThreadToSlack($thread)); } } diff --git a/app/Livewire/Components/ChannelsSelector.php b/app/Livewire/Components/ChannelsSelector.php new file mode 100644 index 00000000..bd161941 --- /dev/null +++ b/app/Livewire/Components/ChannelsSelector.php @@ -0,0 +1,50 @@ +slug = Channel::query()->find($channelId)?->slug; + + $this->dispatch('channelUpdated', channelId: $channelId); + } + + public function resetChannel(): void + { + $this->slug = null; + + $this->dispatch('channelUpdated', channelId: null); + } + + #[Computed] + public function currentChannel(): ?Channel + { + return $this->slug ? Channel::findBySlug($this->slug) : null; + } + + public function render(): View + { + return view('livewire.components.channels-selector', [ + 'channels' => Cache::remember( + 'channels', + now()->addMonth(), + fn () => Channel::with('items')->whereNull('parent_id')->get() + ), + ]); + } +} diff --git a/app/Livewire/Components/Forum/Reply.php b/app/Livewire/Components/Forum/Reply.php new file mode 100644 index 00000000..b63ecd57 --- /dev/null +++ b/app/Livewire/Components/Forum/Reply.php @@ -0,0 +1,88 @@ +label(__('actions.edit')) + ->color('gray') + ->authorize('update', $this->reply) + ->action( + fn () => $this->dispatch( + 'replyForm', + replyId: $this->reply->id + )->to(ReplyForm::class) + ); + } + + public function deleteAction(): Action + { + return Action::make('delete') + ->label(__('actions.delete')) + ->color('danger') + ->authorize('delete', $this->reply) + ->requiresConfirmation() + ->action(function (): void { + $this->reply->delete(); + + $this->redirectRoute('forum.show', $this->thread, navigate: true); + }); + } + + public function solutionAction(): Action + { + return Action::make('solution') + ->label(__('pages/forum.mark_answer')) + ->color('success') + ->authorize('manage', $this->thread) + ->action(function (): void { + if ($this->thread->isSolved()) { + undoPoint(new BestReply($this->thread->solutionReply)); + } + + $this->thread->markSolution($this->reply, Auth::user()); // @phpstan-ignore-line + + givePoint(new BestReply($this->reply)); + + Notification::make() + ->title(__('notifications.thread.best_reply')) + ->success() + ->duration(5000) + ->send(); + + $this->redirect(route('forum.show', $this->thread).$this->reply->getPathUrl(), navigate: true); + }); + } + + #[On('reply.save.{reply.id}')] + public function render(): View + { + return view('livewire.components.forum.reply'); + } +} diff --git a/app/Livewire/Components/Forum/ReplyForm.php b/app/Livewire/Components/Forum/ReplyForm.php new file mode 100644 index 00000000..82731778 --- /dev/null +++ b/app/Livewire/Components/Forum/ReplyForm.php @@ -0,0 +1,121 @@ +form->fill(); + } + + #[On('replyForm')] + public function open(?int $replyId = null): void + { + $this->reply = Reply::query()->find($replyId); + + $this->form->fill(['body' => $this->reply?->body ?? '']); + + $this->show = true; + } + + public function close(): void + { + $this->show = false; + $this->body = null; + $this->reply = null; + } + + public function form(Form $form): Form + { + return $form + ->schema([ + Forms\Components\MarkdownEditor::make('body') + ->hiddenLabel() + ->fileAttachmentsDisk('public') + ->autofocus() + ->toolbarButtons([ + 'attachFiles', + 'blockquote', + 'bold', + 'bulletList', + 'codeBlock', + 'link', + ]), + ]); + } + + public function save(): void + { + $this->validate(); + + if ($this->reply) { + $this->updateReply(); + } else { + $this->createReply(); + } + + $this->redirectRoute('forum.show', $this->thread, navigate: true); + } + + public function createReply(): void + { + $this->authorize('create', Reply::class); + + app(CreateReplyAction::class)->execute( + body: (string) $this->body, + model: $this->thread, + ); + + Notification::make() + ->title(__('notifications.reply.created')) + ->success() + ->duration(5000) + ->send(); + } + + public function updateReply(): void + { + $this->authorize('update', $this->reply); + + $this->reply?->update(['body' => $this->body]); + + Notification::make() + ->title(__('notifications.reply.updated')) + ->success() + ->duration(5000) + ->send(); + } + + public function render(): View + { + return view('livewire.components.forum.reply-form'); + } +} diff --git a/app/Livewire/Components/Forum/Subscribe.php b/app/Livewire/Components/Forum/Subscribe.php new file mode 100644 index 00000000..bcffadaa --- /dev/null +++ b/app/Livewire/Components/Forum/Subscribe.php @@ -0,0 +1,56 @@ +authorize('subscribe', $this->thread); + + app(SubscribeToThreadAction::class)->execute($this->thread); + + Notification::make() + ->title(__('notifications.thread.subscribe')) + ->success() + ->duration(5000) + ->send(); + + $this->dispatch('subscription.update')->self(); + } + + public function unsubscribe(): void + { + $this->authorize('unsubscribe', $this->thread); + + $this->thread->subscribes() + ->where('user_id', Auth::id()) + ->delete(); + + Notification::make() + ->title(__('notifications.thread.unsubscribe')) + ->success() + ->duration(5000) + ->send(); + + $this->dispatch('subscription.update')->self(); + } + + #[On('subscription.update')] + public function render(): View + { + return view('livewire.components.forum.subscribe'); + } +} diff --git a/app/Livewire/Components/Slideovers/ThreadForm.php b/app/Livewire/Components/Slideovers/ThreadForm.php new file mode 100644 index 00000000..8bf899b1 --- /dev/null +++ b/app/Livewire/Components/Slideovers/ThreadForm.php @@ -0,0 +1,149 @@ +thread = $threadId + ? Thread::query()->findOrFail($threadId) + : new Thread; + + $this->form->fill(array_merge( + $this->thread->toArray(), + ['user_id' => $this->thread->user_id ?? Auth::id()] + )); + } + + public static function panelMaxWidth(): string + { + return '2xl'; + } + + public function form(Form $form): Form + { + return $form + ->schema([ + Forms\Components\Hidden::make('user_id'), + Forms\Components\TextInput::make('title') + ->label(__('validation.attributes.title')) + ->helperText(__('pages/forum.max_thread_length')) + ->required() + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, $state, Forms\Set $set): void { + $set('slug', Str::slug($state)); + }) + ->maxLength(100), + Forms\Components\Hidden::make('slug'), + Forms\Components\Select::make('channels') + ->multiple() + ->relationship(titleAttribute: 'name') + ->searchable() + ->required() + ->minItems(1) + ->maxItems(3), + Forms\Components\MarkdownEditor::make('body') + ->fileAttachmentsDisk('public') + ->toolbarButtons([ + 'attachFiles', + 'blockquote', + 'bold', + 'bulletList', + 'codeBlock', + 'link', + ]) + ->label(__('validation.attributes.content')) + ->required() + ->minLength(20), + Forms\Components\Placeholder::make('') + ->content(fn () => new HtmlString(Blade::render(<<<'Blade' +
+ {{ __('pages/forum.torchlight') }} + + Torchlight + +
+ Blade))), + ]) + ->statePath('data') + ->model($this->thread); + } + + public function save(): void + { + // @phpstan-ignore-next-line + if (! Auth::user()->hasVerifiedEmail()) { + throw new UnverifiedUserException( + message: __('notifications.exceptions.unverified_user') + ); + } + + $this->validate(); + + if ($this->thread?->id) { + $this->authorize('update', $this->thread); + } + + if ($this->thread?->id) { + $this->thread->update($this->form->getState()); + $this->form->model($this->thread)->saveRelationships(); + } else { + $thread = Thread::query()->create($this->form->getState()); + $this->form->model($thread)->saveRelationships(); + + app(SubscribeToThreadAction::class)->execute($thread); + + givePoint(new ThreadCreated($thread)); + event(new ThreadWasCreated($thread)); + } + + Notification::make() + ->title( + $this->thread?->id + ? __('notifications.thread.updated') + : __('notifications.thread.created'), + ) + ->success() + ->send(); + + $this->dispatch('thread.save.{$thread->id}'); + + $this->redirect(route('forum.show', ['thread' => $thread ?? $this->thread]), navigate: true); + } + + public function render(): View + { + return view('livewire.components.slideovers.thread-form'); + } +} diff --git a/app/Livewire/Forum/CreateReply.php b/app/Livewire/Forum/CreateReply.php deleted file mode 100644 index ea56229a..00000000 --- a/app/Livewire/Forum/CreateReply.php +++ /dev/null @@ -1,66 +0,0 @@ - 'onMarkdownUpdate']; - - /** - * @var string[] - */ - protected $rules = [ - 'body' => 'required', - ]; - - public function onMarkdownUpdate(string $content): void - { - $this->body = $content; - } - - public function save(): void - { - $this->authorize(ReplyPolicy::CREATE, Reply::class); - - $this->validate(); - - $reply = new Reply(['body' => $this->body]); - $reply->authoredBy(Auth::user()); // @phpstan-ignore-line - $reply->to($this->thread); - $reply->save(); - - givePoint(new ReplyCreated($this->thread, Auth::user())); - - event(new ReplyWasCreated($reply)); - - session()->flash('status', 'Réponse ajoutée avec succès!'); - - $this->redirectRoute('forum.show', $this->thread); - } - - public function render(): View - { - return view('livewire.forum.create-reply'); - } -} diff --git a/app/Livewire/Forum/CreateThread.php b/app/Livewire/Forum/CreateThread.php deleted file mode 100644 index 84f7b3a8..00000000 --- a/app/Livewire/Forum/CreateThread.php +++ /dev/null @@ -1,80 +0,0 @@ - 'onMarkdownUpdate']; - - /** - * @var string[] - */ - protected $rules = [ - 'title' => 'required|max:75', - 'body' => 'required', - ]; - - public function onMarkdownUpdate(string $content): void - { - $this->body = $content; - } - - public function store(): void - { - $this->validate(); - $author = Auth::user(); - - $thread = Thread::create([ - 'title' => $this->title, - 'body' => $this->body, - 'slug' => $this->title, - 'user_id' => $author->id, // @phpstan-ignore-line - ]); - - $thread->syncChannels($this->associateChannels); - - // Subscribe author to the thread. - $subscription = new \App\Models\Subscribe; - $subscription->uuid = Uuid::uuid4()->toString(); - $subscription->user()->associate($author); - $subscription->subscribeAble()->associate($thread); - - $thread->subscribes()->save($subscription); - - givePoint(new ThreadCreated($thread)); - - if (app()->environment('production')) { - event(new ThreadWasCreated($thread)); - } - - $this->redirectRoute('forum.show', $thread); - } - - public function render(): View - { - return view('livewire.forum.create-thread', [ - 'channels' => Channel::all(), - ]); - } -} diff --git a/app/Livewire/Forum/EditThread.php b/app/Livewire/Forum/EditThread.php deleted file mode 100644 index 45054180..00000000 --- a/app/Livewire/Forum/EditThread.php +++ /dev/null @@ -1,69 +0,0 @@ - 'onMarkdownUpdate']; - - /** - * @var string[] - */ - protected $rules = [ - 'title' => 'required|max:75', - 'body' => 'required', - ]; - - public function mount(Thread $thread): void - { - $this->title = $thread->title; - $this->body = $thread->body; - $this->associateChannels = $this->channels_selected = old('channels', $thread->channels()->pluck('id')->toArray()); - } - - public function onMarkdownUpdate(string $content): void - { - $this->body = $content; - } - - public function store(): void - { - $this->validate(); - - $this->thread->update([ - 'title' => $this->title, - 'slug' => $this->title, - 'body' => $this->body, - ]); - - $this->thread->syncChannels($this->associateChannels); - - $this->redirectRoute('forum.show', $this->thread); - } - - public function render(): View - { - return view('livewire.forum.edit-thread', [ - 'channels' => Channel::all(), - ]); - } -} diff --git a/app/Livewire/Forum/Reply.php b/app/Livewire/Forum/Reply.php deleted file mode 100644 index 6f5fa5c1..00000000 --- a/app/Livewire/Forum/Reply.php +++ /dev/null @@ -1,121 +0,0 @@ - '$refresh', - 'editor:update' => 'onEditorUpdate', - ]; - - /** - * @var string[] - */ - protected $rules = [ - 'body' => 'required', - ]; - - public function mount(ReplyModel $reply, Thread $thread): void - { - $this->thread = $thread; - $this->reply = $reply; - $this->body = $reply->body; - } - - public function onEditorUpdate(string $body): void - { - $this->body = $body; - } - - public function edit(): void - { - $this->authorize(ReplyPolicy::UPDATE, $this->reply); - - $this->validate(); - - $this->reply->update(['body' => $this->body]); - - Notification::make() - ->title(__('Réponse modifiée')) - ->body(__('Vous avez modifié cette solution avec succès.')) - ->success() - ->duration(5000) - ->send(); - - $this->isUpdating = false; - - $this->dispatch('refresh')->self(); - } - - public function UnMarkAsSolution(): void - { - $this->authorize(ThreadPolicy::UPDATE, $this->thread); - - undoPoint(new BestReply($this->reply)); - - $this->thread->unmarkSolution(); - - $this->dispatch('refresh')->self(); - - Notification::make() - ->title(__('Réponse rejetée')) - ->body(__('Vous avez retiré cette réponse comme solution pour ce sujet.')) - ->success() - ->duration(5000) - ->send(); - } - - public function markAsSolution(): void - { - $this->authorize(ThreadPolicy::UPDATE, $this->thread); - - if ($this->thread->isSolved()) { - undoPoint(new BestReply($this->thread->solutionReply)); - } - - $this->thread->markSolution($this->reply, Auth::user()); // @phpstan-ignore-line - - givePoint(new BestReply($this->reply)); - - $this->dispatch('refresh')->self(); - - Notification::make() - ->title(__('Réponse acceptée')) - ->body(__('Vous avez accepté cette solution pour ce sujet.')) - ->success() - ->duration(5000) - ->send(); - } - - public function render(): View - { - return view('livewire.forum.reply'); - } -} diff --git a/app/Livewire/Forum/Subscribe.php b/app/Livewire/Forum/Subscribe.php deleted file mode 100644 index 8c1f3c2d..00000000 --- a/app/Livewire/Forum/Subscribe.php +++ /dev/null @@ -1,69 +0,0 @@ - '$refresh']; - - public function subscribe(): void - { - $this->authorize(ThreadPolicy::SUBSCRIBE, $this->thread); - - $subscribe = new SubscribeModel; - $subscribe->uuid = Uuid::uuid4()->toString(); - $subscribe->user()->associate(Auth::user()); - $this->thread->subscribes()->save($subscribe); - - Notification::make() - ->title(__('Abonnement')) - ->body(__('Vous êtes maintenant abonné à ce sujet.')) - ->success() - ->duration(5000) - ->send(); - - $this->dispatch('refresh')->self(); - } - - public function unsubscribe(): void - { - $this->authorize(ThreadPolicy::UNSUBSCRIBE, $this->thread); - - $this->thread->subscribes() - ->where('user_id', Auth::id()) - ->delete(); - - Notification::make() - ->title(__('Désabonnement')) - ->body(__('Vous vous êtes désabonné de ce sujet.')) - ->success() - ->duration(5000) - ->send(); - - $this->dispatch('refresh')->self(); - } - - public function render(): View - { - return view('livewire.forum.subscribe'); - } -} diff --git a/app/Livewire/Modals/DeleteReply.php b/app/Livewire/Modals/DeleteReply.php index a7565d44..b7ed9568 100644 --- a/app/Livewire/Modals/DeleteReply.php +++ b/app/Livewire/Modals/DeleteReply.php @@ -5,7 +5,6 @@ namespace App\Livewire\Modals; use App\Models\Reply; -use App\Policies\ReplyPolicy; use Illuminate\Contracts\View\View; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use LivewireUI\Modal\ModalComponent; @@ -20,15 +19,15 @@ final class DeleteReply extends ModalComponent public function mount(int $id, string $slug): void { - $this->reply = Reply::find($id); + $this->reply = Reply::query()->find($id); $this->slug = $slug; } public function delete(): void { - $this->authorize(ReplyPolicy::DELETE, $this->reply); + $this->authorize('delete', $this->reply); - $this->reply->delete(); // @phpstan-ignore-line + $this->reply?->delete(); session()->flash('status', __('La réponse a ete supprimée avec succès.')); diff --git a/app/Livewire/Modals/DeleteThread.php b/app/Livewire/Modals/DeleteThread.php index cb71a6db..38b2e390 100644 --- a/app/Livewire/Modals/DeleteThread.php +++ b/app/Livewire/Modals/DeleteThread.php @@ -5,7 +5,6 @@ namespace App\Livewire\Modals; use App\Models\Thread; -use App\Policies\ThreadPolicy; use Illuminate\Contracts\View\View; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use LivewireUI\Modal\ModalComponent; @@ -18,7 +17,7 @@ final class DeleteThread extends ModalComponent public function mount(int $id): void { - $this->thread = Thread::find($id); + $this->thread = Thread::query()->find($id); } public static function modalMaxWidth(): string @@ -28,9 +27,9 @@ public static function modalMaxWidth(): string public function delete(): void { - $this->authorize(ThreadPolicy::DELETE, $this->thread); + $this->authorize('delete', $this->thread); - $this->thread->delete(); // @phpstan-ignore-line + $this->thread?->delete(); session()->flash('status', __('Le sujet a été supprimé avec toutes ses réponses.')); diff --git a/app/Livewire/Pages/Forum/Channels.php b/app/Livewire/Pages/Forum/Channels.php new file mode 100644 index 00000000..f9e98ed2 --- /dev/null +++ b/app/Livewire/Pages/Forum/Channels.php @@ -0,0 +1,31 @@ + Channel::query() + ->withCount('threads') + ->whereNull('parent_id') + ->get() + ->sortByDesc('threads_count'), + 'childChannels' => Channel::query() + ->withCount('threads') + ->whereNotNull('parent_id') + ->get() + ->sortByDesc('threads_count'), + ]) + ->title(__('pages/forum.navigation.channels')); + } +} diff --git a/app/Livewire/Pages/Forum/DetailThread.php b/app/Livewire/Pages/Forum/DetailThread.php new file mode 100644 index 00000000..cd0b2df5 --- /dev/null +++ b/app/Livewire/Pages/Forum/DetailThread.php @@ -0,0 +1,68 @@ +cooldown(now()->addHour())->record(); + + $this->thread = $thread->loadCount('views'); + } + + public function editAction(): Action + { + return Action::make('edit') + ->label(__('actions.edit')) + ->color('gray') + ->authorize('update', $this->thread) + ->action( + fn () => $this->dispatch( + 'openPanel', + component: 'components.slideovers.thread-form', + arguments: ['threadId' => $this->thread->id] + ) + ); + } + + public function deleteAction(): Action + { + return Action::make('delete') + ->label(__('actions.delete')) + ->color('danger') + ->authorize('delete', $this->thread) + ->requiresConfirmation() + ->action(function (): void { + $this->thread->delete(); + + $this->redirectRoute('forum.index', navigate: true); + }); + } + + #[On('thread.save.{thread.id}')] + public function render(): View + { + return view('livewire.pages.forum.detail-thread') + ->title($this->thread->subject()); + } +} diff --git a/app/Livewire/Pages/Forum/Index.php b/app/Livewire/Pages/Forum/Index.php new file mode 100644 index 00000000..1ccdcd24 --- /dev/null +++ b/app/Livewire/Pages/Forum/Index.php @@ -0,0 +1,140 @@ +channel) { + $this->currentChannel = Channel::findBySlug($this->channel); + } + } + + #[On('channelUpdated')] + public function reloadThreads(?int $channelId): void + { + if ($channelId) { + $this->currentChannel = Channel::query()->find($channelId); + } else { + $this->currentChannel = null; + } + + $this->resetPage(); + $this->dispatch('render'); + } + + protected function applySearch(Builder $query): Builder + { + if ($this->search) { + return $query->where(function (Builder $query): void { + $query->where('title', 'like', '%'.$this->search.'%'); + }); + } + + return $query; + } + + protected function applySolved(Builder $query): Builder + { + if ($this->solved) { + return match ($this->solved) { + 'no' => $query->scopes('unresolved'), + 'yes' => $query->scopes('resolved'), + }; + } + + return $query; + } + + protected function applyAuthor(Builder $query): Builder + { + if (Auth::check() && $this->user) { + return $query->whereHas('user', function (Builder $query): void { + $query->where('user_id', Auth::id()); + }); + } + + return $query; + } + + protected function applyChannel(Builder $query): Builder + { + if ($this->currentChannel?->id) { + return $query->scopes(['channel' => $this->currentChannel]); + } + + return $query; + } + + protected function applySubscribe(Builder $query): Builder + { + return $query; + } + + protected function applyUnAnswer(Builder $query): Builder + { + return $query; + } + + public function render(): View + { + $query = Thread::with('channels') + ->orderByDesc('created_at'); + + $query = $this->applyChannel($query); + $query = $this->applySearch($query); + $query = $this->applySolved($query); + $query = $this->applyAuthor($query); + $query = $this->applySubscribe($query); + $query = $this->applyUnAnswer($query); + + $threads = $query + ->scopes('withViewsCount') + ->paginate($this->perPage); + + return view('livewire.pages.forum.index', [ + 'threads' => $threads, + ]) + ->title(__('pages/forum.channel_title', ['channel' => isset($this->currentChannel) ? ' ~ '.$this->currentChannel->name : ''])); + } +} diff --git a/app/Livewire/Reactions.php b/app/Livewire/Reactions.php index b787e164..0dd238a0 100644 --- a/app/Livewire/Reactions.php +++ b/app/Livewire/Reactions.php @@ -19,7 +19,7 @@ final class Reactions extends Component public bool $withBackground = true; - public string $direction = 'right'; + public string $direction = 'horizontal'; public function userReacted(string $reaction): void { @@ -33,6 +33,7 @@ public function userReacted(string $reaction): void } else { /** @var Reaction $react */ $react = Reaction::query()->where('name', $reaction)->first(); + Auth::user()->reactTo($this->model, $react); // @phpstan-ignore-line } } diff --git a/app/Livewire/Traits/WithAuthenticatedUser.php b/app/Livewire/Traits/WithAuthenticatedUser.php new file mode 100644 index 00000000..2400d6cb --- /dev/null +++ b/app/Livewire/Traits/WithAuthenticatedUser.php @@ -0,0 +1,17 @@ +redirect(route('login'), navigate: true); + } + } +} diff --git a/app/Mail/NewReplyEmail.php b/app/Mail/NewReplyEmail.php index a71402f6..6f8cde7e 100644 --- a/app/Mail/NewReplyEmail.php +++ b/app/Mail/NewReplyEmail.php @@ -21,6 +21,7 @@ public function __construct( public function build(): self { + // @phpstan-ignore-next-line return $this->subject("Re: {$this->reply->replyAble->subject()}") ->markdown('emails.new_reply'); } diff --git a/app/Models/Activity.php b/app/Models/Activity.php index e8add905..a3475784 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -4,12 +4,24 @@ namespace App\Models; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Support\Collection; +/** + * @property-read int $id + * @property int $user_id + * @property array | null $data + * @property string $subject_type + * @property int $subject_id + * @property string $type + * @property User $user + * @property Carbon $created_at + * @property Carbon $updated_at + */ final class Activity extends Model { use HasFactory; @@ -36,18 +48,14 @@ public function user(): BelongsTo return $this->belongsTo(User::class); } - /** - * @return array