diff --git a/app/Enums/LivewireEventEnum.php b/app/Enums/LivewireEventEnum.php index 6fff9212..1a522b3c 100644 --- a/app/Enums/LivewireEventEnum.php +++ b/app/Enums/LivewireEventEnum.php @@ -6,6 +6,7 @@ enum LivewireEventEnum: string { + case BlurReset = 'blur-reset'; case CommentDeleted = 'comment-deleted'; case CommentFlagCancelled = 'comment-flag-cancelled'; case CommentFlagDeleted = 'comment-flag-deleted'; diff --git a/app/Livewire/Comments/CommentComponent.php b/app/Livewire/Comments/CommentComponent.php index 008a6546..af42b814 100644 --- a/app/Livewire/Comments/CommentComponent.php +++ b/app/Livewire/Comments/CommentComponent.php @@ -7,8 +7,8 @@ use App\Enums\CommentStateEnum; use App\Enums\LivewireEventEnum; use App\Enums\ModerationTypeEnum; -use App\Enums\RoleNameEnum; use App\Models\Comment; +use App\Traits\AuthStatusTrait; use App\Traits\CommentComponentTrait; use Illuminate\Contracts\View\View; use Illuminate\Support\Collection; @@ -17,6 +17,7 @@ final class CommentComponent extends Component { + use AuthStatusTrait; use CommentComponentTrait; // State @@ -28,7 +29,9 @@ public function mount(int $commentId, ?Comment $comment, ?Collection $childComme // and the moderator comments collection. $this->commentId = $commentId; $this->comment = $comment ?? Comment::find($commentId); - $this->childComments = $childComments; + if ($childComments) { + $this->childComments = $childComments; + } // Decide whether to blur the component initially. $this->isBlurred = $this->isInitiallyBlurred; @@ -38,19 +41,23 @@ public function render(): View { $moderationType = $this->appearanceComment?->moderation_type ?? null; + $hasModeratorActions = $this->filterModeratorActions($this->childComments, $this->state === CommentStateEnum::Moderating)->isNotEmpty(); + // If there are no decorations to apply, just render the basic comment component. return view('livewire.comments.comment-component', [ 'comment' => $this->comment, 'childComments' => $this->childComments, 'moderationType' => $moderationType, 'isInitiallyBlurred' => $this->isInitiallyBlurred, - 'isRemoved' => $moderationType === ModerationTypeEnum::Remove && !auth()->user()?->hasRole(RoleNameEnum::MODERATOR->value), + 'isRemoved' => $moderationType === ModerationTypeEnum::Remove && !$this->isModerator(), + 'appearanceComment' => $this->appearanceComment, 'appearanceCommentId' => $this->appearanceComment?->id, 'blurCommentId' => $this->blurComment?->id, 'isEditing' => $this->state === CommentStateEnum::Editing, 'isFlagging' => $this->state === CommentStateEnum::Flagging, 'isReplying' => $this->state === CommentStateEnum::Replying, 'isModerating' => $this->state === CommentStateEnum::Moderating, + 'hasModeratorActions' => $hasModeratorActions, ]); } diff --git a/app/Livewire/Comments/CommentListComponent.php b/app/Livewire/Comments/CommentListComponent.php index 9ee38c0f..82e86329 100644 --- a/app/Livewire/Comments/CommentListComponent.php +++ b/app/Livewire/Comments/CommentListComponent.php @@ -7,6 +7,7 @@ use App\Enums\LivewireEventEnum; use App\Repositories\CommentRepositoryInterface; use App\Traits\AuthStatusTrait; +use App\Traits\ModeratorActionsTrait; use App\Traits\SubsiteTrait; use Illuminate\Contracts\View\View; use Illuminate\Support\Collection; @@ -18,6 +19,7 @@ final class CommentListComponent extends Component { use AuthStatusTrait; + use ModeratorActionsTrait; use SubsiteTrait; #[Locked] @@ -50,6 +52,7 @@ public function render(): View { return view('livewire.comments.comment-list-component', [ 'comments' => $this->comments, + 'isModerator' => $this->isModerator(), ]); } @@ -58,7 +61,7 @@ public function render(): View LivewireEventEnum::CommentDeleted->value, LivewireEventEnum::CommentUpdated->value, ])] - public function getComments(): void + public function refreshComments(): void { unset($this->comments); } diff --git a/app/Livewire/Comments/CommentRemoval.php b/app/Livewire/Comments/CommentRemoval.php deleted file mode 100644 index c55c388d..00000000 --- a/app/Livewire/Comments/CommentRemoval.php +++ /dev/null @@ -1,27 +0,0 @@ - $this->comment, - 'childComments' => $this->childComments, - 'moderatorComment' => $this->appearanceComment, - 'isModerating' => $this->state === CommentStateEnum::Moderating, - ]); - } -} diff --git a/app/Livewire/Comments/CommentReplacement.php b/app/Livewire/Comments/CommentReplacement.php deleted file mode 100644 index 472857a7..00000000 --- a/app/Livewire/Comments/CommentReplacement.php +++ /dev/null @@ -1,27 +0,0 @@ - $this->comment, - 'childComments' => $this->childComments, - 'moderatorComment' => $this->appearanceComment, - 'isModerating' => $this->state === CommentStateEnum::Moderating, - ]); - } -} diff --git a/app/Livewire/Comments/CommentWrapper.php b/app/Livewire/Comments/CommentWrapper.php index 37ead2e8..b57fe803 100644 --- a/app/Livewire/Comments/CommentWrapper.php +++ b/app/Livewire/Comments/CommentWrapper.php @@ -4,6 +4,7 @@ namespace App\Livewire\Comments; +use App\Enums\CommentStateEnum; use App\Traits\CommentComponentTrait; use App\Traits\CommentComponentStateTrait; use Illuminate\Contracts\View\View; @@ -24,6 +25,7 @@ public function render(): View 'childComments' => $this->childComments, 'moderatorComment' => $this->appearanceComment, 'isInitiallyBlurred' => $this->isInitiallyBlurred, + 'isModerating' => $this->state === CommentStateEnum::Moderating, ]); } } diff --git a/app/Livewire/Comments/ModeratorComment.php b/app/Livewire/Comments/ModeratorComment.php new file mode 100644 index 00000000..cef450fe --- /dev/null +++ b/app/Livewire/Comments/ModeratorComment.php @@ -0,0 +1,42 @@ +comment->moderation_type) { + ModerationTypeEnum::Blur => 'blurred', + ModerationTypeEnum::Comment => 'commented', + ModerationTypeEnum::Edit => 'edited', + ModerationTypeEnum::Remove => 'removed', + ModerationTypeEnum::Replace => 'replaced', + ModerationTypeEnum::Wrap => 'wrapped', + ModerationTypeEnum::Restore => 'restored', + default => '', + }; + + return view('livewire.comments.moderator-comment', [ + 'comment' => $this->comment, + 'isModerating' => $this->state === CommentStateEnum::Moderating, + 'moderationAction' => $moderationAction, + 'moderationClass' => 'moderator-' . $moderationAction, + ]); + } +} diff --git a/app/Livewire/Comments/ModeratorCommentListComponent.php b/app/Livewire/Comments/ModeratorCommentListComponent.php new file mode 100644 index 00000000..d82f94c6 --- /dev/null +++ b/app/Livewire/Comments/ModeratorCommentListComponent.php @@ -0,0 +1,85 @@ +commentRepository = $commentRepository; + } + + public function mount(int $parentId, ?Collection $childComments): void + { + $this->parentId = $parentId; + + if ($childComments) { + // If provided, use the child comments from the parent comment. + // This saves some time when all post comments were loaded up front. + $this->childComments = $childComments; + } + } + + /** + * Returns all child comments in chronological order. + */ + #[Computed] + public function childComments(): Collection + { + return $this->commentRepository->getCommentsByParentId($this->parentId); + } + + /** + * Returns all moderator actions in reverse chronological order. + */ + #[Computed] + public function moderatorActions(): Collection + { + return $this->filterModeratorActions($this->childComments, $this->state === CommentStateEnum::Moderating); + } + + public function render(): View + { + return view('livewire.comments.moderator-comment-list-component', [ + 'comments' => $this->moderatorActions, + 'isModerating' => $this->state === CommentStateEnum::Moderating, + 'isModerator' => $this->isModerator(), + ]); + } + + #[On([ + LivewireEventEnum::CommentStored->value, + LivewireEventEnum::CommentDeleted->value, + LivewireEventEnum::CommentUpdated->value, + ])] + public function refreshChildComments(): void + { + unset($this->childComments); + } +} diff --git a/app/Traits/AuthStatusTrait.php b/app/Traits/AuthStatusTrait.php index 2748c90a..c1b61641 100644 --- a/app/Traits/AuthStatusTrait.php +++ b/app/Traits/AuthStatusTrait.php @@ -15,7 +15,7 @@ public function getAuthorizedUserId(): int|null public function isModerator(): bool { - return auth()->user()->hasRole(RoleNameEnum::MODERATOR->value); + return auth()->user()?->hasRole(RoleNameEnum::MODERATOR->value) ?? false; } public function loggedIn(): bool diff --git a/app/Traits/CommentComponentTrait.php b/app/Traits/CommentComponentTrait.php index dd6610ab..4085f89c 100644 --- a/app/Traits/CommentComponentTrait.php +++ b/app/Traits/CommentComponentTrait.php @@ -6,7 +6,6 @@ use App\Enums\CommentStateEnum; use App\Enums\LivewireEventEnum; -use App\Enums\ModerationTypeEnum; use App\Models\Comment; use App\Repositories\CommentRepositoryInterface; use Illuminate\Support\Collection; @@ -16,6 +15,8 @@ trait CommentComponentTrait { + use ModeratorActionsTrait; + // Data #[Locked] public int $commentId = 0; @@ -45,19 +46,7 @@ public function userFlagged(): bool #[Computed] public function appearanceComment(): ?Comment { - $appearanceComment = $this->childComments?->last( - fn($comment) => $comment->moderation_type !== null && match ($comment->moderation_type) { - ModerationTypeEnum::Remove, ModerationTypeEnum::Replace, ModerationTypeEnum::Wrap, ModerationTypeEnum::Restore => true, - default => false, - }, - ); - - // If the last appearance-modifying comment is a Restore, return null. - if ($appearanceComment && $appearanceComment->moderation_type === ModerationTypeEnum::Restore) { - return null; - } - - return $appearanceComment; + return $this->findLastAppearanceComment($this->childComments); } /** @@ -69,19 +58,7 @@ public function appearanceComment(): ?Comment #[Computed] public function blurComment(): ?Comment { - $blurComment = $this->childComments?->last( - fn($comment) => $comment->moderation_type !== null && match ($comment->moderation_type) { - ModerationTypeEnum::Blur, ModerationTypeEnum::Remove, ModerationTypeEnum::Replace, ModerationTypeEnum::Restore => true, - default => false, - }, - ); - - // If the last blur-modifying comment is not a Blur, return null. - if ($blurComment && $blurComment->moderation_type !== ModerationTypeEnum::Blur) { - return null; - } - - return $blurComment; + return $this->findLastBlurComment($this->childComments); } #[Computed] @@ -150,6 +127,11 @@ public function toggleModerating(): void } } + public function resetBlur(): void + { + $this->dispatch(LivewireEventEnum::BlurReset->value, id: $this->commentId); + } + public function requestStateChange(CommentStateEnum $state): void { $this->dispatch(LivewireEventEnum::CommentFormStateChanged->value, id: $this->commentId, state: $state); diff --git a/app/Traits/ModeratorActionsTrait.php b/app/Traits/ModeratorActionsTrait.php new file mode 100644 index 00000000..efdd16eb --- /dev/null +++ b/app/Traits/ModeratorActionsTrait.php @@ -0,0 +1,75 @@ +last( + fn($comment) => $comment->moderation_type !== null && match ($comment->moderation_type) { + ModerationTypeEnum::Remove, ModerationTypeEnum::Replace, ModerationTypeEnum::Wrap, ModerationTypeEnum::Restore => true, + default => false, + }, + ); + + // If the last appearance-modifying comment is a Restore, return null + if ($appearanceComment && $appearanceComment->moderation_type === ModerationTypeEnum::Restore) { + return null; + } + + return $appearanceComment; + } + + /** + * Finds the most recent moderator blur comment. + * + * Blurring of comments can coexist with wrapping, but not with removal or replacement. + * It is also reset by a later Restore comment. + */ + protected function findLastBlurComment(?Collection $comments): ?Comment + { + $blurComment = $comments?->last( + fn($comment) => $comment->moderation_type !== null && match ($comment->moderation_type) { + ModerationTypeEnum::Blur, ModerationTypeEnum::Remove, ModerationTypeEnum::Replace, ModerationTypeEnum::Restore => true, + default => false, + }, + ); + + // If the last blur-modifying comment is not a Blur, return null + if ($blurComment && $blurComment->moderation_type !== ModerationTypeEnum::Blur) { + return null; + } + + return $blurComment; + } + + /** + * Filters the child comments to only include moderator actions that should be displayed. + * + * Moderator action visibility depends on whether we are currently moderating the comment or not. + */ + protected function filterModeratorActions(Collection $childComments, bool $isModerating = false): Collection + { + return $childComments->filter(fn($comment) => $comment->moderation_type !== null && match ($comment->moderation_type) { + ModerationTypeEnum::Comment, ModerationTypeEnum::Edit => true, + ModerationTypeEnum::Blur, ModerationTypeEnum::Remove, ModerationTypeEnum::Replace, ModerationTypeEnum::Wrap, ModerationTypeEnum::Restore => $isModerating, + default => false, + }); + } +} diff --git a/resources/sass/modules/_forms.scss b/resources/sass/modules/_forms.scss index 06f8b489..a313ba57 100644 --- a/resources/sass/modules/_forms.scss +++ b/resources/sass/modules/_forms.scss @@ -243,10 +243,6 @@ form.mode-switcher, height: 2rem; } -main form { - margin: 0 auto 0 auto; -} - .form-group { margin: 0 0 1rem 0; padding: 0; diff --git a/resources/sass/modules/_layout.scss b/resources/sass/modules/_layout.scss index 0a9b5bad..fa0216fc 100644 --- a/resources/sass/modules/_layout.scss +++ b/resources/sass/modules/_layout.scss @@ -68,6 +68,12 @@ body { position: relative; } +.main-contents { + display: flex; + flex-direction: column; + gap: 2rem; +} + .main-contents>section.posts>* { padding-inline: 0.75rem; } diff --git a/resources/sass/modules/_posts.scss b/resources/sass/modules/_posts.scss index 9902dc48..e0332343 100644 --- a/resources/sass/modules/_posts.scss +++ b/resources/sass/modules/_posts.scss @@ -2,6 +2,9 @@ position: relative; padding: 0.75rem; border-radius: 0.25rem; +} + +section.posts>article.post { margin-bottom: 2rem; } @@ -9,7 +12,6 @@ display: flex; flex-direction: column; gap: 2rem; - margin-bottom: 2rem; } .comment { @@ -17,18 +19,23 @@ border-radius: 0.25rem; } +.comment>.comments { + padding: 1rem; + gap: 1rem; +} + .comment.moderator-removed { display: none; } -.comment>.moderator-removed, -.comment>.moderator-replaced, .comment>.moderator-hidden>summary, .comment .comment-container { padding: 0.75rem; border-radius: 0.25rem; } +.comments>.moderator-message, +.comment>.moderator-message, .comment .blur-container { border-radius: 0.25rem; } @@ -40,6 +47,17 @@ gap: 1rem; } +.comment-footer .button-group { + display: flex; + flex-direction: row; + gap: 0.25rem; +} + +.comment-footer .button.footer-button { + padding: 0.5rem; + border-radius: 0.25rem; +} + .comment-metadata { display: flex; align-items: center; @@ -63,6 +81,40 @@ border: 1px solid var(--is-danger); } +div.moderation-action { + display: flex; + flex-direction: row; +} + +div.moderation-action>aside.moderation-action { + flex: 0 0 auto; + align-self: stretch; + padding: 0.25rem; + border-top-left-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.25rem - 1px); + background-color: light-dark(var(--base-color), var(--yellow-green)); + color: light-dark(var(--very-light-gray), var(--base-color)); + writing-mode: sideways-lr; + text-orientation: mixed; + text-transform: uppercase; + text-align: center; + font-size: var(--font-size-minus-1); +} + + +div.moderation-action>div.comment-container { + flex: 1 1 auto; +} + +div.moderation-action.moderator-removed>aside.moderation-action { + background-color: var(--is-danger); + color: var(--white); +} + +form.comment-form small.posting-as { + margin: 0 0 1rem 0; +} + details.moderator-hidden { padding: 0; border-radius: inherit; diff --git a/resources/views/livewire/comments/comment-blur.blade.php b/resources/views/livewire/comments/comment-blur.blade.php index e6f70bf3..70376f10 100644 --- a/resources/views/livewire/comments/comment-blur.blade.php +++ b/resources/views/livewire/comments/comment-blur.blade.php @@ -7,6 +7,7 @@ :comment="$comment" :child-comments="$childComments" :$state + @blur-reset="isBlurred = true" />
@@ -15,11 +16,3 @@ @endif
- -@script - -@endscript \ No newline at end of file diff --git a/resources/views/livewire/comments/comment-component.blade.php b/resources/views/livewire/comments/comment-component.blade.php index 1fc22d41..e3fe1fa4 100644 --- a/resources/views/livewire/comments/comment-component.blade.php +++ b/resources/views/livewire/comments/comment-component.blade.php @@ -8,35 +8,45 @@ data-member-id="{{ $comment->user->id }}" @endif > - @if ($moderationType === ModerationTypeEnum::Remove) - @moderator - - @endmoderator - @elseif ($moderationType === ModerationTypeEnum::Replace) - - @elseif ($moderationType === ModerationTypeEnum::Wrap) - + @elseif ($appearanceComment) + @if ($moderationType === ModerationTypeEnum::Wrap) + + @elseif ($moderationType === ModerationTypeEnum::Remove) + @moderator + + @endmoderator + @else + + @endif @else @if ($isInitiallyBlurred) @endif + + @if ($hasModeratorActions) + + @endif diff --git a/resources/views/livewire/comments/comment-content.blade.php b/resources/views/livewire/comments/comment-content.blade.php index fcd7e034..9de123c9 100644 --- a/resources/views/livewire/comments/comment-content.blade.php +++ b/resources/views/livewire/comments/comment-content.blade.php @@ -12,26 +12,38 @@ ]) - @auth - - - - - @if ($comment->user_id === auth()->id()) - @include('livewire.comments.partials.toggle-editing-button') +
+ @auth + + + + @endauth + + @include('livewire.comments.partials.toggle-flagging-button') + + @if ($this->isInitiallyBlurred) + @include('livewire.comments.partials.toggle-blur-button') @endif +
- @include('livewire.comments.partials.toggle-replying-button') + @auth +
+ @if ($comment->user_id === auth()->id()) + @include('livewire.comments.partials.toggle-editing-button') + @endif - @include('livewire.comments.partials.toggle-moderating-button') - @endauth + @include('livewire.comments.partials.toggle-replying-button') - @include('livewire.comments.partials.toggle-flagging-button') + @include('livewire.comments.partials.toggle-moderating-button', [ + 'commentId' => $comment->id, + ]) +
+ @endauth diff --git a/resources/views/livewire/comments/comment-form-component.blade.php b/resources/views/livewire/comments/comment-form-component.blade.php index 78f6ab0b..3fcdc6b3 100644 --- a/resources/views/livewire/comments/comment-form-component.blade.php +++ b/resources/views/livewire/comments/comment-form-component.blade.php @@ -1,4 +1,5 @@

{{ trans('Comments') }} @@ -14,19 +19,22 @@ @forelse ($rootComments as $comment) @php $childComments = $commentsByParentId->get($comment->id); + $appearanceComment = $this->findLastAppearanceComment($childComments); @endphp - + @if ($appearanceComment?->moderation_type !== ModerationTypeEnum::Remove || $isModerator) + + @endif @empty @include('notifications.none-listed', [ 'records' => $recordsText, ]) @endforelse - + diff --git a/resources/views/livewire/comments/comment-removal.blade.php b/resources/views/livewire/comments/comment-removal.blade.php deleted file mode 100644 index eb74915c..00000000 --- a/resources/views/livewire/comments/comment-removal.blade.php +++ /dev/null @@ -1,18 +0,0 @@ -
-
- {!! $moderatorComment->body !!} -
-
- - - @auth - @include('livewire.comments.partials.toggle-moderating-button') - @endauth -
-
\ No newline at end of file diff --git a/resources/views/livewire/comments/comment-replacement.blade.php b/resources/views/livewire/comments/comment-replacement.blade.php deleted file mode 100644 index 6cd48db6..00000000 --- a/resources/views/livewire/comments/comment-replacement.blade.php +++ /dev/null @@ -1,18 +0,0 @@ -
-
- {!! $moderatorComment->body !!} -
-
- - - @auth - @include('livewire.comments.partials.toggle-moderating-button') - @endauth -
-
\ No newline at end of file diff --git a/resources/views/livewire/comments/comment-wrapper.blade.php b/resources/views/livewire/comments/comment-wrapper.blade.php index 6211ac1a..f3790ac3 100644 --- a/resources/views/livewire/comments/comment-wrapper.blade.php +++ b/resources/views/livewire/comments/comment-wrapper.blade.php @@ -16,6 +16,14 @@ 'comment' => $moderatorComment, ]) + + @moderator +
+ @include('livewire.comments.partials.toggle-moderating-button', [ + 'commentId' => $comment->id, + ]) +
+ @endmoderator