Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
071108f
feat: rejection reason templates, review claims and a decision log fo…
xHeaven Oct 16, 2026
c6b5b27
fix: polish the moderation helpers
xHeaven Oct 16, 2026
800b4ed
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
6486a65
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
93b83c6
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
6a6cef2
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
fc7dd61
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
e2bdffd
Merge production navigation into feat/moderation-helpers
xHeaven Oct 16, 2026
b0454e4
Merge production rail avatars into feat/moderation-helpers
xHeaven Oct 16, 2026
b6f3747
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
82cd8f7
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
46373f0
test: isolate GitHub Action link unfurling
xHeaven Oct 16, 2026
f443628
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
6ed76e4
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
df5aa27
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
ea13df5
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
fbc521a
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
a07ed38
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
1cd2904
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
0ef6361
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
11c1ace
fix: keep public preview counts clean
xHeaven Oct 16, 2026
b94ecbb
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
5c91397
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
7374606
ci: enable search extensions in test databases
xHeaven Oct 16, 2026
febb3e5
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
fbb9015
fix: declare restoration test assertion return types
xHeaven Oct 16, 2026
f0b66c4
fix: complete commit feed test fixture
xHeaven Oct 16, 2026
3922e77
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
2cf5c3a
test: preserve moderation audit when approving world replies
xHeaven Oct 16, 2026
f2e840b
fix: resolve unit test imports with node
xHeaven Oct 16, 2026
d4bcf96
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
d4117d9
Merge production into feat/moderation-helpers
xHeaven Oct 16, 2026
c7dcae1
Merge production AI moderation into feat/moderation-helpers
xHeaven Oct 16, 2026
6b7c69e
feat: semantic searching
nunomaduro Oct 16, 2026
90a4d80
Merge remote-tracking branch 'origin/production' into production
nunomaduro Oct 16, 2026
6a13b03
feat: play YouTube, Suno, Spotify and other links in place (#243)
HelgeSverre Oct 16, 2026
dfdf000
fix: let @world step aside once a real world account takes the name (…
hbenabdallah Oct 16, 2026
3ba5051
feat: report published commits for moderation (#93)
xHeaven Oct 16, 2026
a6528f6
feat: keep attached images in composer drafts (#161)
xHeaven Oct 16, 2026
0f63d40
feat: poll visible commit stars (#143)
xHeaven Oct 16, 2026
ad2288c
fix: build
nunomaduro Oct 16, 2026
20bed3f
Merge production search and reports into feat/moderation-helpers
xHeaven Oct 16, 2026
a0b5bdf
Align moderation helpers with rewritten production
xHeaven Oct 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Identity is GitHub. Every account is a real GitHub user, and the profile (avatar
- Links: URLs and bare domains in a commit or reply link out. Like X, a long one shows shortened: no `https://` or `www.`, the whole host, then at most 20 characters ending in `…`. Hovering shows the full URL, and copying the text gives it. Links in code stay as written. Link previews (title text and image) shorten them the same way.
- Notifications: in-app only, for a new follower, for an approved reply to one of your commits (only the direct parent's author; never for replying to yourself), for an approved quote of one of your commits (never for quoting yourself), and for an approved commit or reply that mentions you (never for mentioning yourself; the parent's author of a reply or the quoted author of a quote hears about it as a reply or quote, not twice). A notification points to the follow, reply, quote, or mention, it never copies it: it disappears when the commit, the commit it replies to or quotes, or either user is deleted, and is hidden while the follow is undone or the commit is rejected. A follower notifies once, ever: unfollowing and following again never pings twice. Pending replies, quotes, and mentions notify nobody until approved. No notification for moderation outcomes. A new follower can be followed back, or unfollowed, right from the list. Unread count in the nav; a page is marked as read once shown.
- People search: find developers by name or GitHub username (⌘K). Commits are not searchable.
- Moderation queue (admins only): admins approve or reject pending commits, oldest first. Rejection reasons are shared templates every admin can create, edit, and delete; picking one (click, or `1`–`9` on the focused commit) prefills the editable reason. Focusing or acting on a commit claims it for 5 minutes, one claim per admin; other admins see who holds it and cannot approve or reject it until it expires or they take it over explicitly. Every admin approval and rejection is logged with the admin and time, listed newest first under Decisions and shown on the commit's page to admins.
- Rate limits, per user: stars and saves 60/min each, pins 20/min, follows 30/min, people lookups (search, mention suggestions, hover cards) 120/min combined, exports 3/hour. Throttled actions keep the page and show a toast; throttled pages show the 429 page.
- Public read API and Atom feeds: read-only, so nothing can be created, starred, or followed through them, and there are no API tokens. Lists are cursor-paginated, 20 per page.
- Deliberately excluded for now: likes/reactions, reposts without a comment of your own (quotes are in), images/media, hashtags, searching commits, DMs, other OAuth providers, a write API or API tokens, moderation tools, blocking/muting, header/banner images. Future work must not design for these as if they exist.
Expand Down
18 changes: 18 additions & 0 deletions app/Actions/CreateRejectionReason.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace App\Actions;

use App\Models\RejectionReason;

final readonly class CreateRejectionReason
{
public function handle(string $title, string $body): RejectionReason
{
return RejectionReason::query()->create([
'title' => $title,
'body' => $body,
]);
}
}
15 changes: 15 additions & 0 deletions app/Actions/DeleteRejectionReason.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace App\Actions;

use App\Models\RejectionReason;

final readonly class DeleteRejectionReason
{
public function handle(RejectionReason $rejectionReason): void
{
$rejectionReason->delete();
}
}
2 changes: 1 addition & 1 deletion app/Actions/UpdateCommitAiDecision.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public function handle(Commit $commit): void
]);

if ($scores !== [] && max($scores) >= self::AUTO_REJECT_THRESHOLD) {
$this->updateCommitStatuses->handle([$commit->id], CommitStatus::Rejected, onlyPending: true);
$this->updateCommitStatuses->handle(null, [$commit->id], CommitStatus::Rejected, onlyPending: true);
}
}
}
41 changes: 41 additions & 0 deletions app/Actions/UpdateCommitClaim.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace App\Actions;

use App\Models\Commit;
use App\Models\CommitClaim;
use App\Models\User;
use Illuminate\Support\Facades\DB;

final readonly class UpdateCommitClaim
{
/**
* Claims the commit for the user, taking it over from whoever held it. A user reviews one commit at a time, so
* their previous claim is released.
*/
public function handle(User $user, Commit $commit): CommitClaim
{
return DB::transaction(function () use ($user, $commit): CommitClaim {
$commitIds = [$commit->id];

if ($commit->series_id !== null) {
$parts = Commit::query()->where('series_id', $commit->series_id);
$commitIds = $parts->pluck('id')->all();
$commit = $parts->pending()->oldest('series_position')->firstOrFail();
}

CommitClaim::query()
->where('user_id', $user->id)
->orWhereIn('commit_id', $commitIds)
->delete();

return CommitClaim::query()->create([
'commit_id' => $commit->id,
'user_id' => $user->id,
'expires_at' => now()->addMinutes(CommitClaim::MINUTES),
]);
});
}
}
25 changes: 0 additions & 25 deletions app/Actions/UpdateCommitEmbedding.php

This file was deleted.

5 changes: 3 additions & 2 deletions app/Actions/UpdateCommitStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Enums\CommitStatus;
use App\Models\Commit;
use App\Models\User;

final readonly class UpdateCommitStatus
{
Expand All @@ -14,8 +15,8 @@ public function __construct(private UpdateCommitStatuses $updateCommitStatuses)
//
}

public function handle(Commit $commit, CommitStatus $status, ?string $reason = null): void
public function handle(User $moderator, Commit $commit, CommitStatus $status, ?string $reason = null): void
{
$this->updateCommitStatuses->handle([$commit->id], $status, $reason);
$this->updateCommitStatuses->handle($moderator, [$commit->id], $status, $reason);
}
}
24 changes: 21 additions & 3 deletions app/Actions/UpdateCommitStatuses.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
use App\Enums\CommitStatus;
use App\Enums\NotificationType;
use App\Models\Commit;
use App\Models\CommitClaim;
use App\Models\CommitDecision;
use App\Models\Notification;
use App\Models\Poll;
use App\Models\User;
use App\Queries\CommitBuilder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
Expand All @@ -26,15 +29,17 @@ public function __construct(
}

/**
* A series is moderated as a whole, so no part is left hanging off a part nobody else can see. Its author hears about it once.
* Moderates a series as a whole and releases its claims. Human decisions are logged; automated rejections have no moderator.
*
* @param array<int, string> $commitIds
*/
public function handle(array $commitIds, CommitStatus $status, ?string $reason = null, bool $onlyPending = false): void
public function handle(?User $moderator, array $commitIds, CommitStatus $status, ?string $reason = null, bool $onlyPending = false): void
{
assert($status !== CommitStatus::Pending);

DB::transaction(function () use ($commitIds, $status, $reason, $onlyPending): void {
$reason = $status === CommitStatus::Rejected ? $reason : null;

DB::transaction(function () use ($moderator, $commitIds, $status, $reason, $onlyPending): void {
$commits = Commit::query()
->where(fn (CommitBuilder $query): CommitBuilder => $query
->whereKey($commitIds)
Expand All @@ -51,6 +56,19 @@ public function handle(array $commitIds, CommitStatus $status, ?string $reason =

Commit::query()->whereKey($commits->modelKeys())->update(['status' => $status]);

if ($moderator instanceof User) {
CommitDecision::query()->insert($commits->map(fn (Commit $commit): array => [
'commit_id' => $commit->id,
'user_id' => $moderator->id,
'status' => $status->value,
'reason' => $reason,
'created_at' => now(),
'updated_at' => now(),
])->all());
}

CommitClaim::query()->whereIn('commit_id', $commits->modelKeys())->delete();

Notification::query()
->whereIn('commit_id', $commits->modelKeys())
->whereIn('type', [NotificationType::Approved, NotificationType::Rejected])
Expand Down
18 changes: 18 additions & 0 deletions app/Actions/UpdateRejectionReason.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace App\Actions;

use App\Models\RejectionReason;

final readonly class UpdateRejectionReason
{
public function handle(RejectionReason $rejectionReason, string $title, string $body): void
{
$rejectionReason->update([
'title' => $title,
'body' => $body,
]);
}
}
61 changes: 0 additions & 61 deletions app/Console/Commands/EmbedCommitsCommand.php

This file was deleted.

6 changes: 4 additions & 2 deletions app/Http/Controllers/CommitApprovalController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
use App\Enums\CommitStatus;
use App\Http\Requests\CreateCommitApprovalRequest;
use App\Models\Commit;
use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Http\RedirectResponse;

final readonly class CommitApprovalController
{
public function store(CreateCommitApprovalRequest $request, Commit $commit, UpdateCommitStatus $action): RedirectResponse
public function store(CreateCommitApprovalRequest $request, #[CurrentUser] User $user, Commit $commit, UpdateCommitStatus $action): RedirectResponse
{
$action->handle($commit, CommitStatus::Approved);
$action->handle($user, $commit, CommitStatus::Approved);

return back();
}
Expand Down
22 changes: 22 additions & 0 deletions app/Http/Controllers/CommitClaimController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Actions\UpdateCommitClaim;
use App\Http\Requests\UpdateCommitClaimRequest;
use App\Models\Commit;
use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Http\RedirectResponse;

final readonly class CommitClaimController
{
public function update(UpdateCommitClaimRequest $request, #[CurrentUser] User $user, Commit $commit, UpdateCommitClaim $action): RedirectResponse
{
$action->handle($user, $commit);

return back();
}
}
1 change: 1 addition & 0 deletions app/Http/Controllers/CommitController.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function show(ShowCommitRequest $request, #[CurrentUser] ?User $currentUs
'ancestors' => $isInSeries ? $series->before($commit, $currentUser) : $ancestors->get($commit, $currentUser),
'series' => $series->after($commit, $currentUser),
'replies' => $threadReplies,
'decisions' => $request->showsDecisions() ? $commit->decisions()->with('user')->latest('id')->get() : [],
])->withViewData(['preview' => $preview]);
}

Expand Down
21 changes: 21 additions & 0 deletions app/Http/Controllers/CommitDecisionController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Http\Requests\ListCommitDecisionsRequest;
use App\Queries\CommitDecisionsQuery;
use Illuminate\Contracts\Pagination\CursorPaginator;
use Inertia\Inertia;
use Inertia\Response;

final readonly class CommitDecisionController
{
public function index(ListCommitDecisionsRequest $request, CommitDecisionsQuery $decisions): Response
{
return Inertia::render('moderation/decisions', [
'decisions' => Inertia::scroll(fn (): CursorPaginator => $decisions->paginate()),
]);
}
}
6 changes: 4 additions & 2 deletions app/Http/Controllers/CommitRejectionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,18 @@
use App\Enums\CommitStatus;
use App\Http\Requests\CreateCommitRejectionRequest;
use App\Models\Commit;
use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Http\RedirectResponse;

final readonly class CommitRejectionController
{
public function store(CreateCommitRejectionRequest $request, Commit $commit, UpdateCommitStatus $action): RedirectResponse
public function store(CreateCommitRejectionRequest $request, #[CurrentUser] User $user, Commit $commit, UpdateCommitStatus $action): RedirectResponse
{
/** @var string|null $reason */
$reason = $request->validated('reason');

$action->handle($commit, CommitStatus::Rejected, $reason);
$action->handle($user, $commit, CommitStatus::Rejected, $reason);

return back();
}
Expand Down
21 changes: 0 additions & 21 deletions app/Http/Controllers/CommitSearchController.php

This file was deleted.

Loading
Loading