From 972f82f28c50db35a406d8670bb25bf35b42f0ac Mon Sep 17 00:00:00 2001 From: Arthur Monney Date: Fri, 10 Mar 2023 08:35:01 +0100 Subject: [PATCH 1/4] :recycle: move discussions comment from react to livewire --- app/Actions/Replies/CreateReply.php | 30 ++ app/Actions/Replies/LikeReply.php | 20 + app/Http/Livewire/Discussions/AddComment.php | 56 +++ app/Http/Livewire/Discussions/Comment.php | 43 ++ app/Http/Livewire/Discussions/Comments.php | 44 ++ app/Http/Livewire/Discussions/Subscribe.php | 17 +- app/Models/Discussion.php | 2 +- app/Models/Reply.php | 4 +- app/Traits/HasProfilePhoto.php | 19 +- app/Traits/HasSubscribers.php | 4 +- app/View/Composers/AuthUserComposer.php | 17 + app/View/Composers/ChannelsComposer.php | 4 +- .../Composers/InactiveDiscussionsComposer.php | 6 +- app/View/Composers/ModeratorsComposer.php | 4 +- app/View/Composers/ProfileUsersComposer.php | 6 +- .../Composers/TopContributorsComposer.php | 6 +- app/View/Composers/TopMembersComposer.php | 4 +- composer.json | 1 + composer.lock | 149 +++++- package.json | 2 - resources/css/app.css | 1 - resources/js/api/comments.js | 59 --- resources/js/api/premium.js | 6 +- resources/js/components/Button.jsx | 2 +- resources/js/components/Comments.jsx | 474 ------------------ resources/js/components/Icon.jsx | 31 -- resources/js/components/Loader.jsx | 3 - resources/js/components/Markdown.jsx | 39 -- resources/js/elements/index.js | 2 - resources/js/helpers.js | 8 - resources/views/discussions/show.blade.php | 8 +- .../discussions/add-comment.blade.php | 57 +++ .../livewire/discussions/comment.blade.php | 61 +++ .../livewire/discussions/comments.blade.php | 12 + .../livewire/discussions/subscribe.blade.php | 4 +- routes/console.php | 19 - 36 files changed, 539 insertions(+), 685 deletions(-) create mode 100644 app/Actions/Replies/CreateReply.php create mode 100644 app/Actions/Replies/LikeReply.php create mode 100644 app/Http/Livewire/Discussions/AddComment.php create mode 100644 app/Http/Livewire/Discussions/Comment.php create mode 100644 app/Http/Livewire/Discussions/Comments.php create mode 100644 app/View/Composers/AuthUserComposer.php delete mode 100644 resources/js/api/comments.js delete mode 100644 resources/js/components/Comments.jsx delete mode 100644 resources/js/components/Icon.jsx delete mode 100644 resources/js/components/Markdown.jsx create mode 100644 resources/views/livewire/discussions/add-comment.blade.php create mode 100644 resources/views/livewire/discussions/comment.blade.php create mode 100644 resources/views/livewire/discussions/comments.blade.php delete mode 100644 routes/console.php diff --git a/app/Actions/Replies/CreateReply.php b/app/Actions/Replies/CreateReply.php new file mode 100644 index 00000000..04cd76ba --- /dev/null +++ b/app/Actions/Replies/CreateReply.php @@ -0,0 +1,30 @@ + $body]); + $reply->authoredBy($user); + $reply->to($model); + $reply->save(); + + $user->givePoint(new ReplyCreated($model, $user)); + + // On envoie un event pour une nouvelle réponse à tous les abonnés de la discussion + event(new CommentWasAdded($reply, $model)); + + return $reply; + } +} diff --git a/app/Actions/Replies/LikeReply.php b/app/Actions/Replies/LikeReply.php new file mode 100644 index 00000000..b6707385 --- /dev/null +++ b/app/Actions/Replies/LikeReply.php @@ -0,0 +1,20 @@ +first(); + + $user->reactTo($reply, $react); + } +} diff --git a/app/Http/Livewire/Discussions/AddComment.php b/app/Http/Livewire/Discussions/AddComment.php new file mode 100644 index 00000000..0e33ef1a --- /dev/null +++ b/app/Http/Livewire/Discussions/AddComment.php @@ -0,0 +1,56 @@ + '$refresh']; + + public function mount(Discussion $discussion): void + { + $this->discussion = $discussion; + } + + public function saveComment(): void + { + $this->validate( + ['body' => 'required'], + ['body.required' => __('Votre commentaire ne peut pas être vide')] + ); + + $comment = CreateReply::run($this->body, auth()->user(), $this->discussion); + + $this->emitSelf('reloadComment'); + + $this->emitUp('reloadComments'); + + $this->dispatchBrowserEvent('scrollToComment', ['id' => $comment->id]); + + Notification::make() + ->title(__('Votre commentaire a été ajouté!')) + ->success() + ->duration(5000) + ->send(); + + $this->reset(); + } + + public function render(): View + { + return view('livewire.discussions.add-comment'); + } +} diff --git a/app/Http/Livewire/Discussions/Comment.php b/app/Http/Livewire/Discussions/Comment.php new file mode 100644 index 00000000..2dcd7dd6 --- /dev/null +++ b/app/Http/Livewire/Discussions/Comment.php @@ -0,0 +1,43 @@ + '$refresh']; + + public function delete(): void + { + $this->comment->delete(); + + Notification::make() + ->title(__('Votre commentaire a été supprimé.')) + ->success() + ->duration(5000) + ->send(); + + $this->emitUp('reloadComment'); + } + + public function toggleLike(): void + { + LikeReply::run(auth()->user(), $this->comment); + + $this->emitSelf('reloadComment'); + } + + public function render(): View + { + return view('livewire.discussions.comment', [ + 'count' => $this->comment->getReactionsSummary()->sum('count'), + ]); + } +} diff --git a/app/Http/Livewire/Discussions/Comments.php b/app/Http/Livewire/Discussions/Comments.php new file mode 100644 index 00000000..b8757961 --- /dev/null +++ b/app/Http/Livewire/Discussions/Comments.php @@ -0,0 +1,44 @@ + '$refresh']; + + public function mount(Discussion $discussion): void + { + $this->discussion = $discussion; + } + + public function getCommentsProperty(): Collection + { + $replies = collect(); + + foreach ($this->discussion->replies->load(['allChildReplies', 'author']) as $reply) { + /** @var Reply $reply */ + if ($reply->allChildReplies->isNotEmpty()) { + foreach ($reply->allChildReplies as $childReply) { + $replies->add($childReply); + } + } + + $replies->add($reply); + } + + return $replies; + } + + public function render(): View + { + return view('livewire.discussions.comments'); + } +} diff --git a/app/Http/Livewire/Discussions/Subscribe.php b/app/Http/Livewire/Discussions/Subscribe.php index 4c36a4ee..72b2fce2 100644 --- a/app/Http/Livewire/Discussions/Subscribe.php +++ b/app/Http/Livewire/Discussions/Subscribe.php @@ -5,6 +5,7 @@ use App\Models\Discussion; use App\Models\Subscribe as SubscribeModel; use App\Policies\DiscussionPolicy; +use Filament\Notifications\Notification; use Illuminate\Contracts\View\View; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Facades\Auth; @@ -28,7 +29,13 @@ public function subscribe() $subscribe->user()->associate(Auth::user()); $this->discussion->subscribes()->save($subscribe); - // @ToDo Mettre un nouveau system de notification avec Livewire $this->notification()->success('Abonnement', 'Vous êtes maintenant abonné à cette discussion.'); + Notification::make() + ->title(__('Abonnement')) + ->body(__('Vous êtes maintenant abonné à cette discussion.')) + ->success() + ->duration(5000) + ->send(); + $this->emitSelf('refresh'); } @@ -40,7 +47,13 @@ public function unsubscribe() ->where('user_id', Auth::id()) ->delete(); - // @ToDo Mettre un nouveau system de notification $this->notification()->success('Désabonnement', 'Vous êtes maintenant désabonné de cette discussion.'); + Notification::make() + ->title(__('Désabonnement')) + ->body(__('Vous êtes maintenant désabonné à cette discussion.')) + ->success() + ->duration(5000) + ->send(); + $this->emitSelf('refresh'); } diff --git a/app/Models/Discussion.php b/app/Models/Discussion.php index ec92562d..3ba2dae0 100644 --- a/app/Models/Discussion.php +++ b/app/Models/Discussion.php @@ -125,7 +125,7 @@ public function isLocked(): bool public function getCountAllRepliesWithChildAttribute(): int { - $count = $this->replies()->count(); + $count = $this->replies->count(); foreach ($this->replies()->withCount('allChildReplies')->get() as $reply) { $count += $reply->all_child_replies_count; diff --git a/app/Models/Reply.php b/app/Models/Reply.php index 3185274f..bba71a6f 100644 --- a/app/Models/Reply.php +++ b/app/Models/Reply.php @@ -62,7 +62,7 @@ public function solutionTo(): HasOne return $this->hasOne(Thread::class, 'solution_reply_id'); } - public function wasJustPublished() + public function wasJustPublished(): bool { return $this->created_at->gt(Carbon::now()->subMinute()); } @@ -72,7 +72,7 @@ public function excerpt(int $limit = 100): string return Str::limit(strip_tags(md_to_html($this->body)), $limit); } - public function mentionedUsers() + public function mentionedUsers(): array { preg_match_all('/@([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w))/', $this->body, $matches); diff --git a/app/Traits/HasProfilePhoto.php b/app/Traits/HasProfilePhoto.php index 58b3d56d..95c301c3 100644 --- a/app/Traits/HasProfilePhoto.php +++ b/app/Traits/HasProfilePhoto.php @@ -4,31 +4,24 @@ trait HasProfilePhoto { - /** - * Get the URL to the user's profile photo. - * - * @return string - */ public function getProfilePhotoUrlAttribute(): string { if ($this->avatar_type === 'storage') { return $this->getFirstMediaUrl('avatar'); } - $social_avatar = $this->providers->where('provider', $this->avatar_type)->first(); + if (!in_array($this->avatar_type, ['avatar', 'storage'])) { + $social_avatar = $this->providers->firstWhere('provider', $this->avatar_type); - if ($social_avatar && strlen($social_avatar->avatar)) { - return $social_avatar->avatar; + if ($social_avatar && strlen($social_avatar->avatar)) { + return $social_avatar->avatar; + } } + return $this->defaultProfilePhotoUrl(); } - /** - * Get the default profile photo URL if no profile photo has been uploaded. - * - * @return string - */ protected function defaultProfilePhotoUrl(): string { return 'https://ui-avatars.com/api/?name='.urlencode($this->name).'&color=065F46&background=D1FAE5'; diff --git a/app/Traits/HasSubscribers.php b/app/Traits/HasSubscribers.php index eeb353e7..d02253ce 100644 --- a/app/Traits/HasSubscribers.php +++ b/app/Traits/HasSubscribers.php @@ -21,8 +21,6 @@ public function subscribes(): MorphMany public function hasSubscriber(User $user): bool { - return $this->subscribes() - ->where('user_id', $user->id) - ->exists(); + return in_array($user->id, $this->subscribes->pluck('user_id')->toArray()); } } diff --git a/app/View/Composers/AuthUserComposer.php b/app/View/Composers/AuthUserComposer.php new file mode 100644 index 00000000..9a77b509 --- /dev/null +++ b/app/View/Composers/AuthUserComposer.php @@ -0,0 +1,17 @@ +check()) { + $view->with('authenticate', auth()->user()); + } else { + $view->with('authenticate', null); + } + } +} diff --git a/app/View/Composers/ChannelsComposer.php b/app/View/Composers/ChannelsComposer.php index d5188547..57269c27 100644 --- a/app/View/Composers/ChannelsComposer.php +++ b/app/View/Composers/ChannelsComposer.php @@ -6,9 +6,9 @@ use Illuminate\Support\Facades\Cache; use Illuminate\View\View; -class ChannelsComposer +final class ChannelsComposer { - public function compose(View $view) + public function compose(View $view): void { $view->with( 'channels', diff --git a/app/View/Composers/InactiveDiscussionsComposer.php b/app/View/Composers/InactiveDiscussionsComposer.php index 944036f0..4e9b6416 100644 --- a/app/View/Composers/InactiveDiscussionsComposer.php +++ b/app/View/Composers/InactiveDiscussionsComposer.php @@ -6,11 +6,11 @@ use Illuminate\Support\Facades\Cache; use Illuminate\View\View; -class InactiveDiscussionsComposer +final class InactiveDiscussionsComposer { - public function compose(View $view) + public function compose(View $view): void { - $discussions = Cache::remember('inactive-discussions', 60 * 30, function () { + $discussions = Cache::remember('inactive_discussions', now()->addDays(3), function () { return Discussion::noComments()->limit(5)->get(); }); diff --git a/app/View/Composers/ModeratorsComposer.php b/app/View/Composers/ModeratorsComposer.php index 55cbd04b..0ded0d39 100644 --- a/app/View/Composers/ModeratorsComposer.php +++ b/app/View/Composers/ModeratorsComposer.php @@ -6,9 +6,9 @@ use Illuminate\Support\Facades\Cache; use Illuminate\View\View; -class ModeratorsComposer +final class ModeratorsComposer { - public function compose(View $view) + public function compose(View $view): void { $view->with('moderators', Cache::remember('moderators', now()->addMinutes(30), function () { return User::moderators()->get(); diff --git a/app/View/Composers/ProfileUsersComposer.php b/app/View/Composers/ProfileUsersComposer.php index 68b5e457..4f63a834 100644 --- a/app/View/Composers/ProfileUsersComposer.php +++ b/app/View/Composers/ProfileUsersComposer.php @@ -6,11 +6,11 @@ use Illuminate\Support\Facades\Cache; use Illuminate\View\View; -class ProfileUsersComposer +final class ProfileUsersComposer { - public function compose(View $view) + public function compose(View $view): void { - $view->with('users', Cache::remember('avatar_users', now()->addDay(), function () { + $view->with('users', Cache::remember('avatar_users', now()->addWeek(), function () { return User::verifiedUsers()->inRandomOrder()->take(10)->get(); })); } diff --git a/app/View/Composers/TopContributorsComposer.php b/app/View/Composers/TopContributorsComposer.php index fb343844..67614d13 100644 --- a/app/View/Composers/TopContributorsComposer.php +++ b/app/View/Composers/TopContributorsComposer.php @@ -6,14 +6,14 @@ use Illuminate\Support\Facades\Cache; use Illuminate\View\View; -class TopContributorsComposer +final class TopContributorsComposer { public function compose(View $view): void { - $topContributors = Cache::remember('contributors', 60 * 30, function () { + $topContributors = Cache::remember('contributors', now()->addWeek(), function () { return User::topContributors() ->get() - ->filter(fn ($contributor) => $contributor->discussions_count >= 1) + ->filter(fn ($contributor) => $contributor->loadCount('discussions')->discussions_count >= 1) ->take(5); }); diff --git a/app/View/Composers/TopMembersComposer.php b/app/View/Composers/TopMembersComposer.php index e7bc55d1..c2d4230b 100644 --- a/app/View/Composers/TopMembersComposer.php +++ b/app/View/Composers/TopMembersComposer.php @@ -6,11 +6,11 @@ use Illuminate\Support\Facades\Cache; use Illuminate\View\View; -class TopMembersComposer +final class TopMembersComposer { public function compose(View $view): void { - $view->with('topMembers', Cache::remember('topMembers', now()->addMinutes(30), function () { + $view->with('topMembers', Cache::remember('topMembers', now()->addWeek(), function () { return User::mostSolutionsInLastDays(365)->take(5)->get(); })); } diff --git a/composer.json b/composer.json index 2f87f4bc..de3187da 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "laravel/socialite": "^5.2", "laravel/tinker": "^2.5", "livewire/livewire": "^2.11", + "lorisleiva/laravel-actions": "^2.5", "nnjeim/world": "^1.1", "qcod/laravel-gamify": "^1.0.6", "ramsey/uuid": "^4.2", diff --git a/composer.lock b/composer.lock index 913d849c..449867ee 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2f0a924f6c99612ab375243346bdf7b5", + "content-hash": "ed2b5d4ce5362dee276593bd52c74548", "packages": [ { "name": "abraham/twitteroauth", @@ -4821,6 +4821,153 @@ ], "time": "2023-01-15T23:43:31+00:00" }, + { + "name": "lorisleiva/laravel-actions", + "version": "v2.5.1", + "source": { + "type": "git", + "url": "https://github.com/lorisleiva/laravel-actions.git", + "reference": "f1937625a342362ca4e4ab5705c93db95e4df634" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lorisleiva/laravel-actions/zipball/f1937625a342362ca4e4ab5705c93db95e4df634", + "reference": "f1937625a342362ca4e4ab5705c93db95e4df634", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^8.15 || 9.0 - 9.34 || ^9.36 || ^10.0", + "lorisleiva/lody": "^0.4.0", + "php": "^8.0|^8.1" + }, + "require-dev": { + "orchestra/testbench": "^8.0", + "pestphp/pest": "^1.2", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Lorisleiva\\Actions\\ActionServiceProvider" + ], + "aliases": { + "Action": "Lorisleiva\\Actions\\Facades\\Actions" + } + } + }, + "autoload": { + "psr-4": { + "Lorisleiva\\Actions\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Loris Leiva", + "email": "loris.leiva@gmail.com", + "homepage": "https://lorisleiva.com", + "role": "Developer" + } + ], + "description": "Laravel components that take care of one specific task", + "homepage": "https://github.com/lorisleiva/laravel-actions", + "keywords": [ + "action", + "command", + "component", + "controller", + "job", + "laravel", + "object" + ], + "support": { + "issues": "https://github.com/lorisleiva/laravel-actions/issues", + "source": "https://github.com/lorisleiva/laravel-actions/tree/v2.5.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/lorisleiva", + "type": "github" + } + ], + "time": "2023-02-22T10:37:59+00:00" + }, + { + "name": "lorisleiva/lody", + "version": "v0.4.0", + "source": { + "type": "git", + "url": "https://github.com/lorisleiva/lody.git", + "reference": "1a43e8e423f3b2b64119542bc44a2071208fae16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lorisleiva/lody/zipball/1a43e8e423f3b2b64119542bc44a2071208fae16", + "reference": "1a43e8e423f3b2b64119542bc44a2071208fae16", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^8.0|^9.0|^10.0", + "php": "^8.0" + }, + "require-dev": { + "orchestra/testbench": "^8.0", + "pestphp/pest": "^1.20.0", + "phpunit/phpunit": "^9.5.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Lorisleiva\\Lody\\LodyServiceProvider" + ], + "aliases": { + "Lody": "Lorisleiva\\Lody\\Lody" + } + } + }, + "autoload": { + "psr-4": { + "Lorisleiva\\Lody\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Loris Leiva", + "email": "loris.leiva@gmail.com", + "homepage": "https://lorisleiva.com", + "role": "Developer" + } + ], + "description": "Load files and classes as lazy collections in Laravel.", + "homepage": "https://github.com/lorisleiva/lody", + "keywords": [ + "classes", + "collection", + "files", + "laravel", + "load" + ], + "support": { + "issues": "https://github.com/lorisleiva/lody/issues", + "source": "https://github.com/lorisleiva/lody/tree/v0.4.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/lorisleiva", + "type": "github" + } + ], + "time": "2023-02-05T15:03:45+00:00" + }, { "name": "maennchen/zipstream-php", "version": "v2.4.0", diff --git a/package.json b/package.json index 15e82357..7eb5a759 100644 --- a/package.json +++ b/package.json @@ -38,10 +38,8 @@ "axios": "^0.21.1", "canvas-confetti": "^1.4.0", "choices.js": "^9.0.1", - "highlight.js": "^10.7.1", "htm": "^3.1.0", "intl-tel-input": "^17.0.13", - "markdown-to-jsx": "^7.1.3", "preact": "^10.5.15", "react-content-loader": "^6.0.3" } diff --git a/resources/css/app.css b/resources/css/app.css index 4ba85c10..b946d6b5 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -3,7 +3,6 @@ @import "tailwindcss/utilities"; @import '../../vendor/filament/forms/dist/module.esm.css'; -@import "~highlight.js/styles/atom-one-dark.css"; @import '~choices.js/public/assets/styles/choices.css'; @import '~intl-tel-input/build/css/intlTelInput.css'; diff --git a/resources/js/api/comments.js b/resources/js/api/comments.js deleted file mode 100644 index ec6ac089..00000000 --- a/resources/js/api/comments.js +++ /dev/null @@ -1,59 +0,0 @@ -import { jsonFetch } from '@helpers/api.js' - -/** - * Représentation d'un commentaire de l'API - * @typedef {{id: number, username: string, avatar: string, content: string, createdAt: number, replies: ReplyResource[]}} ReplyResource - */ - -/** - * @param {number} target - * @return {Promise} - */ -export async function findAllReplies (target) { - return jsonFetch(`/api/replies/${target}`) -} - -/** - * @param {{target: number, user_id: int, body: string}} body - * @return {Promise} - */ -export async function addReply (body) { - return jsonFetch('/api/replies', { - method: 'POST', - body - }) -} - -/** - * @param {int} id - * @param {int} userId - * @return {Promise} - */ -export async function likeReply(id, userId) { - return jsonFetch(`/api/like/${id}`, { - method: 'POST', - body: JSON.stringify({ userId }) - }) -} - -/** - * @param {int} id - * @return {Promise} - */ -export async function deleteReply (id) { - return jsonFetch(`/api/replies/${id}`, { - method: 'DELETE' - }) -} - -/** - * @param {int} id - * @param {string} body - * @return {Promise} - */ -export async function updateReply (id, body) { - return jsonFetch(`/api/replies/${id}`, { - method: 'PUT', - body: JSON.stringify({ body }) - }) -} diff --git a/resources/js/api/premium.js b/resources/js/api/premium.js index 386f14cd..7ffb5f88 100644 --- a/resources/js/api/premium.js +++ b/resources/js/api/premium.js @@ -1,8 +1,8 @@ -import { jsonFetch } from '@helpers/api.js'; +import { jsonFetch } from '@helpers/api.js' /** * @return {Promise} */ export async function findPremiumUsers () { - return jsonFetch(`/api/premium-users`); -} \ No newline at end of file + return jsonFetch(`/api/premium-users`) +} diff --git a/resources/js/components/Button.jsx b/resources/js/components/Button.jsx index a00b8fac..57bf028e 100644 --- a/resources/js/components/Button.jsx +++ b/resources/js/components/Button.jsx @@ -1,4 +1,4 @@ -import Loader from '@components/Loader'; +import Loader from '@components/Loader' import { classNames } from '@helpers/dom.js' export function PrimaryButton ({ children, ...props }) { diff --git a/resources/js/components/Comments.jsx b/resources/js/components/Comments.jsx deleted file mode 100644 index 7dc3f950..00000000 --- a/resources/js/components/Comments.jsx +++ /dev/null @@ -1,474 +0,0 @@ -import { memo } from 'preact/compat' -import ContentLoader from 'react-content-loader' -import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks' -import { ChatBubbleLeftIcon } from '@heroicons/react/24/solid' - -import { findAllReplies, addReply, updateReply, deleteReply, likeReply } from '@api/comments'; -import { DefaultButton, PrimaryButton } from '@components/Button'; -import { Field } from '@components/Form' -import { Markdown } from '@components/Markdown' -import { HeartIcon } from '@components/Icon' -import { canManage, currentUser, isAuthenticated, getUserId } from '@helpers/auth' -import { scrollTo } from '@helpers/animation' -import { catchViolations } from '@helpers/api' -import { classNames } from '@helpers/dom' -import { useVisibility, useAsyncEffect } from '@helpers/hooks' - -/** - * Affiche les commentaires associés à un contenu - * - * @param {{target: number}} param - */ -export function Comments ({ target, parent }) { - target = parseInt(target, 10) - const element = useRef(null) - const [state, setState] = useState({ - editing: null, // ID du commentaire en cours d'édition - comments: null, // Liste des commentaires - focus: null, // Commentaire à focus - reply: null // Commentaire auquel on souhaite répondre - }) - const isVisible = useVisibility(parent) - const comments = useMemo(() => { - if (state.comments === null) { - return null - } - return state.comments.filter(c => c.model_type === 'discussion') - }, [state.comments]) - - // Trouve les commentaires enfant d'un commentaire - function repliesFor (comment) { - return state.comments.filter(c => c.model_type === 'reply' && c.model_id === comment.id) - } - - // On commence l'édition d'un commentaire - const handleEdit = useCallback(comment => { - setState(s => ({ ...s, editing: s.editing === comment.id ? null : comment.id })) - }, []) - - // On met à jour (via l'API un commentaire) - const handleUpdate = useCallback(async (comment, body) => { - const newComment = { ...(await updateReply(comment.id, body)), parent: comment.model_id } - setState(s => ({ - ...s, - editing: null, - comments: s.comments.map(c => (c === comment ? newComment : c)) - })) - }, []) - - // On supprime un commentaire - const handleDelete = useCallback(async comment => { - await deleteReply(comment.id) - setState(s => ({ - ...s, - comments: s.comments.filter(c => c !== comment) - })) - }, []) - - // On répond à un commentaire - const handleReply = useCallback(comment => { - setState(s => ({ ...s, reply: comment.id })) - }, []) - const handleCancelReply = useCallback(() => { - setState(s => ({ ...s, reply: null })) - }, []) - - // On crée un nouveau commentaire - const handleCreate = useCallback( - async (data, parent) => { - data = { ...data, target, parent, user_id: getUserId() } - const newComment = await addReply(data) - setState(s => ({ - ...s, - focus: newComment.id, - reply: null, - comments: [...s.comments, newComment] - })) - }, - [target] - ) - - // On like un commentaire - const handleLike = useCallback(async (comment) => { - const likeComment = await likeReply(comment.id, getUserId()) - setState(s => ({ - ...s, - editing: null, - comments: s.comments.map(c => (c === comment ? likeComment : c)) - })) - }, []) - - // Scroll jusqu'à l'élément si l'ancre commence par un "c" - useAsyncEffect(async () => { - if (window.location.hash.startsWith('#c')) { - const comments = await findAllReplies(target) - setState(s => ({ - ...s, - comments, - focus: window.location.hash.replace('#c', '') - })) - } - }, [element]) - - // On charge les commentaires dès l'affichage du composant - useAsyncEffect(async () => { - if (isVisible) { - const comments = await findAllReplies(target) - setState(s => ({ ...s, comments })) - } - }, [target, isVisible]) - - // On se focalise sur un commentaire - useEffect(() => { - if (state.focus && comments) { - scrollTo(document.getElementById(`c${state.focus}`)) - setState(s => ({ ...s, focus: null })) - } - }, [state.focus, comments]) - - return ( -
-
- {isAuthenticated() ? ( - - ) : ( -
-
-
- - -
- - Commenter - -
-
-
-
-

Veuillez vous connecter ou {' '} - créer un compte pour participer à cette conversation.

-
-
- )} -
-
- {comments ? ( -
    - {comments.map((comment) => ( - -
      - {repliesFor(comment).map(reply => ( - - {state.reply === comment.id && ( - - )} - - ))} -
    -
    - ) - )} -
- ) : ( - <> - - - - )} -
-
- ); -} - -const FakeComment = memo(() => { - return ( - - - - - - - - - ) -}) - -/** - * Affiche un commentaire - */ -const Comment = memo(({ comment, editing, onEdit, onUpdate, onDelete, onReply, onLike, children, isReply }) => { - const anchor = `#c${comment.id}` - const canEdit = canManage(comment.author.id) - const className = ['comment'] - const textarea = useRef(null) - const [loading, setLoading] = useState(false) - - const handleEdit = canEdit - ? e => { - e.preventDefault() - onEdit(comment) - } - : null - - async function handleUpdate (e) { - e.preventDefault() - setLoading(true) - await onUpdate(comment, textarea.current.value) - setLoading(false) - } - - async function handleLike (e) { - e.preventDefault() - if (isAuthenticated()) { - await onLike(comment) - } else { - window.$wireui.notify({ - title: 'Ops! Erreur', - description: 'Vous devez être connecté pour liker ce contenu!', - icon: 'error' - }) - } - } - - async function handleDelete (e) { - e.preventDefault() - if (confirm('Voulez vous vraiment supprimer ce commentaire ?')) { - setLoading(true) - await onDelete(comment) - } - } - - function handleReply (e) { - e.preventDefault() - onReply(comment) - } - - // On focus automatiquement le champs quand il devient visible - useEffect(() => { - if (textarea.current) { - textarea.current.focus() - } - }, [editing]) - - let content = ( - <> -
- -
-
- - {/*{!isReply && ( - - - )}*/} -
- - ) - - if (editing) { - content = ( -
- -