diff --git a/app/Enums/ModerationTypeEnum.php b/app/Enums/ModerationTypeEnum.php
index a4a66cf2..dc4e8037 100644
--- a/app/Enums/ModerationTypeEnum.php
+++ b/app/Enums/ModerationTypeEnum.php
@@ -11,6 +11,7 @@ enum ModerationTypeEnum: string
case Edit = 'edit';
case Remove = 'remove';
case Replace = 'replace';
+ case Restore = 'restore';
case Wrap = 'wrap';
public function label(): string
@@ -21,6 +22,7 @@ public function label(): string
self::Edit => 'Edit',
self::Remove => 'Remove',
self::Replace => 'Replace',
+ self::Restore => 'Restore',
self::Wrap => 'Wrap',
};
}
diff --git a/app/Livewire/Comments/CommentBlur.php b/app/Livewire/Comments/CommentBlur.php
index 0ecaffd7..b351941d 100644
--- a/app/Livewire/Comments/CommentBlur.php
+++ b/app/Livewire/Comments/CommentBlur.php
@@ -4,7 +4,6 @@
namespace App\Livewire\Comments;
-use App\Enums\ModerationTypeEnum;
use App\Traits\CommentComponentTrait;
use App\Traits\CommentComponentStateTrait;
use Illuminate\Contracts\View\View;
@@ -20,12 +19,10 @@ final class CommentBlur extends Component
public function render(): View
{
- $moderatorComment = $this->moderatorCommentsByType?->get(ModerationTypeEnum::Blur->value);
-
return view('livewire.comments.comment-blur', [
'comment' => $this->comment,
'childComments' => $this->childComments,
- 'blurMessage' => $moderatorComment?->body ?? '',
+ 'blurMessage' => $this->blurComment?->body ?? '',
]);
}
}
diff --git a/app/Livewire/Comments/CommentComponent.php b/app/Livewire/Comments/CommentComponent.php
index 3f78da63..008a6546 100644
--- a/app/Livewire/Comments/CommentComponent.php
+++ b/app/Livewire/Comments/CommentComponent.php
@@ -7,11 +7,11 @@
use App\Enums\CommentStateEnum;
use App\Enums\LivewireEventEnum;
use App\Enums\ModerationTypeEnum;
+use App\Enums\RoleNameEnum;
use App\Models\Comment;
use App\Traits\CommentComponentTrait;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
-use Livewire\Attributes\Computed;
use Livewire\Attributes\On;
use Livewire\Component;
@@ -22,12 +22,6 @@ final class CommentComponent extends Component
// State
public CommentStateEnum $state = CommentStateEnum::Viewing;
- #[Computed]
- public function isInitiallyBlurred(): bool
- {
- return $this->moderatorCommentsByType?->get(ModerationTypeEnum::Blur->value) !== null;
- }
-
public function mount(int $commentId, ?Comment $comment, ?Collection $childComments): void
{
// On mount we expect the comment list to provide the comment model
@@ -42,21 +36,7 @@ public function mount(int $commentId, ?Comment $comment, ?Collection $childComme
public function render(): View
{
- // If the comment has been replaced, just render the moderation message.
- // TODO: if it is possible to have a top-level comment be marked with a
- // moderation type, we may want to render it via this view. But it may
- // also just be about changing the border for a regular rendering.
- $moderatorReplaceComment = $this->moderatorCommentsByType?->get(ModerationTypeEnum::Replace->value);
- $moderatorWrapComment = $this->moderatorCommentsByType?->get(ModerationTypeEnum::Wrap->value);
- $moderatorBlurComment = $this->moderatorCommentsByType?->get(ModerationTypeEnum::Blur->value);
- $moderationType = null;
-
- if ($moderatorReplaceComment !== null &&
- ($moderatorWrapComment === null || $moderatorReplaceComment->created_at > $moderatorWrapComment->created_at)) {
- $moderationType = ModerationTypeEnum::Replace;
- } elseif ($moderatorWrapComment !== null) {
- $moderationType = ModerationTypeEnum::Wrap;
- }
+ $moderationType = $this->appearanceComment?->moderation_type ?? null;
// If there are no decorations to apply, just render the basic comment component.
return view('livewire.comments.comment-component', [
@@ -64,9 +44,9 @@ public function render(): View
'childComments' => $this->childComments,
'moderationType' => $moderationType,
'isInitiallyBlurred' => $this->isInitiallyBlurred,
- 'replacedByCommentId' => $moderatorReplaceComment?->id,
- 'wrappedByCommentId' => $moderatorWrapComment?->id,
- 'blurredByCommentId' => $moderatorBlurComment?->id,
+ 'isRemoved' => $moderationType === ModerationTypeEnum::Remove && !auth()->user()?->hasRole(RoleNameEnum::MODERATOR->value),
+ 'appearanceCommentId' => $this->appearanceComment?->id,
+ 'blurCommentId' => $this->blurComment?->id,
'isEditing' => $this->state === CommentStateEnum::Editing,
'isFlagging' => $this->state === CommentStateEnum::Flagging,
'isReplying' => $this->state === CommentStateEnum::Replying,
@@ -90,7 +70,7 @@ public function reloadChildComments(int $id, ?int $parentId): void
{
if ($parentId === $this->commentId) {
$this->childComments = $this->commentRepository->getCommentsByParentId($parentId);
- unset($this->moderatorCommentsByType, $this->isInitiallyBlurred);
+ unset($this->appearanceComment, $this->blurComment, $this->isInitiallyBlurred);
// Re-evaluate whether the comment should be blurred.
$this->isBlurred = $this->isInitiallyBlurred;
diff --git a/app/Livewire/Comments/CommentFormComponent.php b/app/Livewire/Comments/CommentFormComponent.php
index f8b315ca..acf18230 100644
--- a/app/Livewire/Comments/CommentFormComponent.php
+++ b/app/Livewire/Comments/CommentFormComponent.php
@@ -119,11 +119,13 @@ public function render(): View
if ($this->isModerating) {
$data['bodyLabel'] = trans('Original comment');
$data['buttonText'] = trans(match ($this->moderationType) {
+ ModerationTypeEnum::Comment => 'Comment as moderator',
ModerationTypeEnum::Edit => 'Edit comment',
- ModerationTypeEnum::Remove => 'Remove comment',
- ModerationTypeEnum::Replace => 'Replace comment',
- ModerationTypeEnum::Wrap => 'Wrap comment',
ModerationTypeEnum::Blur => 'Blur comment',
+ ModerationTypeEnum::Wrap => 'Wrap comment',
+ ModerationTypeEnum::Replace => 'Replace comment',
+ ModerationTypeEnum::Remove => 'Remove comment',
+ ModerationTypeEnum::Restore => 'Restore comment',
default => 'Moderate',
});
} elseif ($this->isReplying) {
diff --git a/app/Livewire/Comments/CommentRemoval.php b/app/Livewire/Comments/CommentRemoval.php
new file mode 100644
index 00000000..c55c388d
--- /dev/null
+++ b/app/Livewire/Comments/CommentRemoval.php
@@ -0,0 +1,27 @@
+ $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
index c8f4c3d5..472857a7 100644
--- a/app/Livewire/Comments/CommentReplacement.php
+++ b/app/Livewire/Comments/CommentReplacement.php
@@ -5,7 +5,6 @@
namespace App\Livewire\Comments;
use App\Enums\CommentStateEnum;
-use App\Enums\ModerationTypeEnum;
use App\Traits\CommentComponentTrait;
use App\Traits\CommentComponentStateTrait;
use Illuminate\Contracts\View\View;
@@ -18,12 +17,10 @@ final class CommentReplacement extends Component
public function render(): View
{
- $moderatorComment = $this->moderatorCommentsByType?->get(ModerationTypeEnum::Replace->value);
-
return view('livewire.comments.comment-replacement', [
'comment' => $this->comment,
'childComments' => $this->childComments,
- 'moderatorComment' => $moderatorComment,
+ 'moderatorComment' => $this->appearanceComment,
'isModerating' => $this->state === CommentStateEnum::Moderating,
]);
}
diff --git a/app/Livewire/Comments/CommentWrapper.php b/app/Livewire/Comments/CommentWrapper.php
index 3f372fb7..37ead2e8 100644
--- a/app/Livewire/Comments/CommentWrapper.php
+++ b/app/Livewire/Comments/CommentWrapper.php
@@ -4,7 +4,6 @@
namespace App\Livewire\Comments;
-use App\Enums\ModerationTypeEnum;
use App\Traits\CommentComponentTrait;
use App\Traits\CommentComponentStateTrait;
use Illuminate\Contracts\View\View;
@@ -20,12 +19,10 @@ final class CommentWrapper extends Component
public function render(): View
{
- $moderatorComment = $this->moderatorCommentsByType?->get(ModerationTypeEnum::Wrap->value);
-
return view('livewire.comments.comment-wrapper', [
'comment' => $this->comment,
'childComments' => $this->childComments,
- 'moderatorComment' => $moderatorComment,
+ 'moderatorComment' => $this->appearanceComment,
'isInitiallyBlurred' => $this->isInitiallyBlurred,
]);
}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 85c9f88d..80055ebb 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -4,11 +4,13 @@
namespace App\Providers;
+use App\Enums\RoleNameEnum;
use App\Traits\LoggingTrait;
use App\Traits\SubsiteTrait;
use App\Traits\UrlTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
+use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
@@ -30,6 +32,9 @@ public function boot(): void
} catch (NotFoundExceptionInterface|ContainerExceptionInterface $exception) {
$this->logError($exception);
}
+ Blade::if('moderator', function () {
+ return auth()?->user()?->hasRole(RoleNameEnum::MODERATOR->value);
+ });
Model::shouldBeStrict();
diff --git a/app/Traits/CommentComponentTrait.php b/app/Traits/CommentComponentTrait.php
index f8f50f1f..dd6610ab 100644
--- a/app/Traits/CommentComponentTrait.php
+++ b/app/Traits/CommentComponentTrait.php
@@ -36,20 +36,58 @@ public function userFlagged(): bool
return $this->comment?->userFlagged() ?? false;
}
+ /**
+ * Finds the most recent moderator remove, replace, or wrap comment.
+ *
+ * The effect of Reset is to override any previous appearance-modifying comment
+ * and restore the default appearance for the original comment.
+ */
#[Computed]
- public function moderatorCommentsByType(): ?Collection
- {
- return $this->childComments?->filter(
- fn($comment) =>
- $comment->moderation_type !== null &&
- $comment->moderation_type->value !== ModerationTypeEnum::Comment->value,
- )->keyBy(fn($comment) => $comment->moderation_type->value ?? 'none');
+ 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;
+ }
+
+ /**
+ * 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.
+ */
+ #[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;
}
#[Computed]
public function isInitiallyBlurred(): bool
{
- return $this->moderatorCommentsByType?->get(ModerationTypeEnum::Blur->value) !== null;
+ return $this->blurComment !== null;
}
protected CommentRepositoryInterface $commentRepository;
diff --git a/database/migrations/2025_07_25_023003_add_reset_to_comment_moderation_type_enum.php b/database/migrations/2025_07_25_023003_add_reset_to_comment_moderation_type_enum.php
new file mode 100644
index 00000000..a9a59324
--- /dev/null
+++ b/database/migrations/2025_07_25_023003_add_reset_to_comment_moderation_type_enum.php
@@ -0,0 +1,23 @@
+enum('moderation_type', ['blur', 'comment', 'edit', 'remove', 'replace', 'restore', 'wrap'])->nullable()->change();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('comments', function (Blueprint $table) {
+ $table->enum('moderation_type', ['blur', 'comment', 'edit', 'remove', 'replace', 'wrap'])->nullable()->change();
+ });
+ }
+};
diff --git a/resources/sass/modules/_posts.scss b/resources/sass/modules/_posts.scss
index 6c8be33a..9902dc48 100644
--- a/resources/sass/modules/_posts.scss
+++ b/resources/sass/modules/_posts.scss
@@ -17,6 +17,11 @@
border-radius: 0.25rem;
}
+.comment.moderator-removed {
+ display: none;
+}
+
+.comment>.moderator-removed,
.comment>.moderator-replaced,
.comment>.moderator-hidden>summary,
.comment .comment-container {
@@ -48,9 +53,16 @@
.moderator-message {
border: 1px solid light-dark(var(--base-color), var(--yellow-green));
+}
+
+.moderator-message:not(.moderator-removed) .moderator-content {
color: light-dark(var(--base-color), var(--yellow-green));
}
+.moderator-message.moderator-removed {
+ border: 1px solid var(--is-danger);
+}
+
details.moderator-hidden {
padding: 0;
border-radius: inherit;
diff --git a/resources/sass/themes/modules/_themeable.scss b/resources/sass/themes/modules/_themeable.scss
index b684e677..aaa183ea 100644
--- a/resources/sass/themes/modules/_themeable.scss
+++ b/resources/sass/themes/modules/_themeable.scss
@@ -24,6 +24,10 @@ body {
background-color: light-dark(var(--very-light-gray), var(--dark-base-color));
}
+.comment:has(>.moderator-removed) {
+ background-color: inherit;
+}
+
.main-contents>.post {
background-color: inherit;
}
diff --git a/resources/views/livewire/comments/comment-component.blade.php b/resources/views/livewire/comments/comment-component.blade.php
index 7cdc1ad2..1fc22d41 100644
--- a/resources/views/livewire/comments/comment-component.blade.php
+++ b/resources/views/livewire/comments/comment-component.blade.php
@@ -2,13 +2,26 @@
use App\Enums\ModerationTypeEnum;
@endphp
-