Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,8 @@ Quiet, rectangular, decisive.
### Commit Row (signature)
The graph rail, then avatar (40px circle), then a header line: author name (600), mono handle, a middle dot, mono relative time, and the 7-character short hash pushed right (from sm). The body follows at 15px, pre-wrapped. Under the body, a quiet star: a 16px outline icon in Muted Ink, filled in Commit Blue once starred, with the count in mono 13px beside it when the count is at least 1. It is hidden on commits the viewer cannot star. The toggle updates immediately and rolls back if the request fails, without reloading the log. Your own rows reveal a trash icon on hover/focus (always visible on touch). Copy link and Report sit at the right of the action row and reveal the same way, a reply's only while that reply is hovered; on phones they live in a "…" menu. Deleting is inline: a soft red strip asks "Delete this commit? There's no undo." with Cancel and Delete; confirming swaps the body for a mono `rm {hash} → /dev/null` status, then the row collapses (height and opacity, 300ms after a 400ms hold).

**Source badge:** When present, the creating client appears immediately after the relative time as a quiet, non-interactive 4px-cornered Muted Ink label in 12px Geist Mono. The frontend renders the backend-provided source verbatim; current values include `web`, `cli`, `tui`, `ios`, `android`, `macos`, `windows`, and `linux`. Hovering it opens the standard metadata tooltip with the literal copy `Committed via {source}`; the label remains available as its accessible name. Missing attribution shows no label. Thread position and moderation labels follow it. Compact replies, quotes, search results, and scheduled commits use the same label at 11px; headers wrap metadata below the author on narrow widths rather than truncate a known source.

### Graph Rail (signature)
A 14px circle (16px on the landing page) centered on a 2px vertical line. The segment above a node takes the previous author's lane; the segment below takes this author's. The first commit hangs from a Rail Gray segment beneath the staged composer. The end of history is a hollow **root** node with no tail and a muted line ("That's the root commit. There's nothing before it."). A commit you made in the last 15 seconds animates in with `node-in` (scale 0 to 1.25 to 1 with a lane-colored pulse, 700ms, expo-out); landing-page segments draw downward with `rail-draw`, staggered 140ms. Both respect reduced motion.

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

use App\Dto\NewPoll;
use App\Dto\ThreadEngagement;
use App\Enums\CommitSource;
use App\Enums\CommitStatus;
use App\Jobs\CheckUserLevel;
use App\Jobs\EmbedCommit;
Expand Down Expand Up @@ -55,15 +56,15 @@ public function __construct(
* @param array<int, string|null> $imageDescriptions
* @param array<string, string> $replyRecipients
*/
public function handle(User $user, string $body, ?Commit $parent = null, ?Commit $quotedCommit = null, array $images = [], array $imageDescriptions = [], bool|string $linkPreview = true, ?NewPoll $poll = null, array $replyRecipients = [], ?Organization $organization = null): Commit
public function handle(User $user, string $body, ?Commit $parent = null, ?Commit $quotedCommit = null, array $images = [], array $imageDescriptions = [], bool|string $linkPreview = true, ?NewPoll $poll = null, array $replyRecipients = [], ?Organization $organization = null, ?CommitSource $source = null): Commit
{
$url = match ($linkPreview) {
true => $this->urls->first($body),
false => null,
default => $linkPreview,
};

return DB::transaction(function () use ($user, $body, $parent, $quotedCommit, $images, $imageDescriptions, $url, $poll, $replyRecipients, $organization): Commit {
return DB::transaction(function () use ($user, $body, $parent, $quotedCommit, $images, $imageDescriptions, $url, $poll, $replyRecipients, $organization, $source): Commit {
$linkPreview = $this->linkPreviewFor($url);
$createdAt = now();
$hasGoodReputation = $user->hasGoodReputation();
Expand All @@ -73,6 +74,7 @@ public function handle(User $user, string $body, ?Commit $parent = null, ?Commit
'user_id' => $user->id,
'author_id' => $organization->id ?? $user->id,
'body' => $this->emojiShortcodes->replace($body),
'source' => $source,
'mentions' => [...$replyRecipients, ...$mentions],
'parent_id' => $parent?->id,
'root_id' => $parent?->threadRootId(),
Expand Down
6 changes: 5 additions & 1 deletion app/Actions/CreateExport.php
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,15 @@ public function handle(User $user): array
}

/**
* Streams every commit the user owns, including deleted ones, with its recorded source and cached translations.
*
* @return Generator<int, array<string, mixed>>
*/
private function commits(User $user): Generator
{
$commits = $user->commits()
->withTrashed()
->select(['id', 'parent_id', 'quoted_commit_id', 'body', 'status', 'pasteis_count', 'created_at', 'updated_at', 'deleted_at'])
->select(['id', 'parent_id', 'quoted_commit_id', 'body', 'source', 'status', 'pasteis_count', 'created_at', 'updated_at', 'deleted_at'])
->with('images:id,commit_id')
->lazyById(500);

Expand All @@ -204,6 +206,7 @@ private function commits(User $user): Generator
'parent_id' => $commit->parent_id,
'quoted_commit_id' => $commit->quoted_commit_id,
'body' => $commit->body,
'source' => $commit->source?->value,
'cached_translations' => $cached[$translations->key($commit)],
'status' => $commit->status->value,
'pasteis_count' => $commit->pasteis_count,
Expand Down Expand Up @@ -238,6 +241,7 @@ private function organizationCommits(User $user): Generator
'parent_id' => $commit->parent_id,
'quoted_commit_id' => $commit->quoted_commit_id,
'body' => $commit->body,
'source' => $commit->source?->value,
'status' => $commit->status->value,
'images' => $commit->images->map(fn (CommitImage $image): string => $image->url())->all(),
'created_at' => $commit->created_at->toIso8601String(),
Expand Down
4 changes: 3 additions & 1 deletion app/Actions/CreateScheduledCommit.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Actions;

use App\Dto\NewPoll;
use App\Enums\CommitSource;
use App\Models\Organization;
use App\Models\ScheduledCommit;
use App\Models\User;
Expand All @@ -27,14 +28,15 @@ public function __construct(private CreateImageFiles $createImageFiles, private
* @param list<UploadedFile> $images
* @param array<int, string|null> $imageDescriptions
*/
public function handle(User $user, string $body, CarbonImmutable $scheduledAt, array $images = [], array $imageDescriptions = [], ?NewPoll $poll = null, bool|string $linkPreview = true, ?Organization $organization = null): ScheduledCommit
public function handle(User $user, string $body, CarbonImmutable $scheduledAt, array $images = [], array $imageDescriptions = [], ?NewPoll $poll = null, bool|string $linkPreview = true, ?Organization $organization = null, ?CommitSource $source = null): ScheduledCommit
{
$body = mb_trim($body);

return DB::transaction(fn (): ScheduledCommit => ScheduledCommit::query()->create([
'user_id' => $user->id,
'author_id' => $organization?->id,
'body' => $body,
'source' => $source,
'poll' => $poll instanceof NewPoll ? ['options' => $poll->options, 'duration' => $poll->duration->value] : null,
'images' => $images === [] ? null : $this->createImageFiles->handle($images, $imageDescriptions),
'link_preview_url' => match ($linkPreview) {
Expand Down
2 changes: 1 addition & 1 deletion app/Actions/UpdateScheduledCommitSubmittedAt.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public function handle(string $id, bool $early = false): bool
$scheduled = ScheduledCommit::query()->with(['user', 'author'])->findOrFail($id);
assert(is_string($scheduled->body));
$poll = $scheduled->poll === null ? null : new NewPoll($scheduled->poll['options'], PollDuration::from($scheduled->poll['duration']));
$commit = $this->createCommit->handle($scheduled->user, $scheduled->body, linkPreview: $scheduled->link_preview_url ?? false, poll: $poll, organization: $scheduled->author?->organization);
$commit = $this->createCommit->handle($scheduled->user, $scheduled->body, linkPreview: $scheduled->link_preview_url ?? false, poll: $poll, organization: $scheduled->author?->organization, source: $scheduled->source);
$commit->images()->createMany($scheduled->images ?? []);

$scheduled->forceFill(['commit_id' => $commit->id, 'body' => null, 'poll' => null, 'images' => null, 'link_preview_url' => null])->save();
Expand Down
17 changes: 17 additions & 0 deletions app/Enums/CommitSource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace App\Enums;

enum CommitSource: string
{
case Web = 'web';
case Cli = 'cli';
case Tui = 'tui';
case Ios = 'ios';
case Android = 'android';
case Macos = 'macos';
case Windows = 'windows';
case Linux = 'linux';
}
5 changes: 3 additions & 2 deletions app/Http/Controllers/Api/CommitController.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use App\Models\User;
use App\Queries\CommitAncestorsQuery;
use App\Queries\CommitPageQuery;
use App\Services\CommitSourceResolver;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
Expand Down Expand Up @@ -49,9 +50,9 @@ public function show(ShowApiCommitRequest $request, #[CurrentUser] ?User $curren
* Answers with the new commit as its page shows it, with the commit it quotes and its poll, pending review unless
* the user is an admin.
*/
public function store(CreateApiCommitRequest $request, #[CurrentUser] User $user, CreateCommit $action, CommitPageQuery $page): JsonResponse
public function store(CreateApiCommitRequest $request, #[CurrentUser] User $user, CreateCommit $action, CommitPageQuery $page, CommitSourceResolver $sources): JsonResponse
{
$commit = $action->handle($user, $request->string('body')->value(), $request->parent(), $request->quotedCommit(), $request->images(), $request->imageDescriptions(), poll: $request->poll(), replyRecipients: $request->replyRecipients(), organization: $request->organization());
$commit = $action->handle($user, $request->string('body')->value(), $request->parent(), $request->quotedCommit(), $request->images(), $request->imageDescriptions(), poll: $request->poll(), replyRecipients: $request->replyRecipients(), organization: $request->organization(), source: $sources->from($request));

return CommitResource::make($page->get($commit, $user))
->response()
Expand Down
5 changes: 3 additions & 2 deletions app/Http/Controllers/Api/ScheduledCommitController.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use App\Models\ScheduledCommit;
use App\Models\User;
use App\Queries\ScheduledCommitsQuery;
use App\Services\CommitSourceResolver;
use Carbon\CarbonImmutable;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Http\JsonResponse;
Expand All @@ -31,9 +32,9 @@ public function index(#[CurrentUser] User $user, ScheduledCommitsQuery $schedule
/**
* Only the user sees a scheduled commit until it is submitted.
*/
public function store(CreateApiScheduledCommitRequest $request, #[CurrentUser] User $user, CreateScheduledCommit $action): JsonResponse
public function store(CreateApiScheduledCommitRequest $request, #[CurrentUser] User $user, CreateScheduledCommit $action, CommitSourceResolver $sources): JsonResponse
{
$scheduled = $action->handle($user, $request->string('body')->value(), CarbonImmutable::parse($request->string('scheduled_at')->value())->utc(), $request->images(), $request->imageDescriptions(), $request->poll(), $request->linkPreview(), $request->organization());
$scheduled = $action->handle($user, $request->string('body')->value(), CarbonImmutable::parse($request->string('scheduled_at')->value())->utc(), $request->images(), $request->imageDescriptions(), $request->poll(), $request->linkPreview(), $request->organization(), $sources->from($request));

return ScheduledCommitResource::make($scheduled)
->response()
Expand Down
6 changes: 5 additions & 1 deletion app/Http/Controllers/Cli/CommitController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Http\Controllers\Cli;

use App\Actions\CreateCommit;
use App\Enums\CommitSource;
use App\Enums\CommitStatus;
use App\Http\Requests\CreateCliCommitRequest;
use App\Models\User;
Expand All @@ -13,9 +14,12 @@

final readonly class CommitController
{
/**
* Creates a CLI commit and returns its URL, noting when it is awaiting moderation.
*/
public function store(CreateCliCommitRequest $request, #[CurrentUser] User $user, CreateCommit $action): Response
{
$commit = $action->handle($user, $request->string('body')->value(), $request->parent(), images: $request->images());
$commit = $action->handle($user, $request->string('body')->value(), $request->parent(), images: $request->images(), source: CommitSource::Cli);

$url = route('commit.show', [$user, $commit]);

Expand Down
6 changes: 5 additions & 1 deletion app/Http/Controllers/CommitController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Actions\CreateCommit;
use App\Actions\DeleteCommit;
use App\Dto\ThreadContinuation;
use App\Enums\CommitSource;
use App\Enums\CommitStatus;
use App\Http\Requests\CreateCommitRequest;
use App\Http\Requests\DeleteCommitRequest;
Expand Down Expand Up @@ -63,12 +64,15 @@ public function show(ShowCommitRequest $request, #[CurrentUser] ?User $currentUs
])->withViewData(['preview' => $preview]);
}

/**
* Creates a web commit and flashes any achievements it unlocks for its author.
*/
public function store(CreateCommitRequest $request, #[CurrentUser] User $user, CreateCommit $action, UserAchievementsQuery $achievements): RedirectResponse
{
$before = $achievements->unlockedBadgeNames($user);

$organization = $request->organization();
$commit = $action->handle($user, $request->string('body')->value(), quotedCommit: $request->quotedCommit(), images: $request->images(), imageDescriptions: $request->imageDescriptions(), linkPreview: $request->linkPreview(), poll: $request->poll(), organization: $organization);
$commit = $action->handle($user, $request->string('body')->value(), quotedCommit: $request->quotedCommit(), images: $request->images(), imageDescriptions: $request->imageDescriptions(), linkPreview: $request->linkPreview(), poll: $request->poll(), organization: $organization, source: CommitSource::Web);

$unlocked = $achievements->unlockedBadgeNames($user)->diff($before)->values();

Expand Down
3 changes: 2 additions & 1 deletion app/Http/Controllers/CommitReplyController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Actions\CreateCommit;
use App\Actions\DeleteCommit;
use App\Enums\CommitSource;
use App\Enums\CommitStatus;
use App\Http\Requests\CreateCommitReplyRequest;
use App\Http\Requests\DeleteCommitReplyRequest;
Expand All @@ -28,7 +29,7 @@ public function index(ListCommitRepliesRequest $request, #[CurrentUser] ?User $u
public function store(CreateCommitReplyRequest $request, #[CurrentUser] User $user, Commit $commit, CreateCommit $action, CommitThreadQuery $thread): JsonResponse
{
$organization = $request->organization();
$reply = $action->handle($user, $request->string('body')->value(), $commit, images: $request->images(), imageDescriptions: $request->imageDescriptions(), linkPreview: $request->linkPreview(), replyRecipients: $request->replyRecipients(), organization: $organization);
$reply = $action->handle($user, $request->string('body')->value(), $commit, images: $request->images(), imageDescriptions: $request->imageDescriptions(), linkPreview: $request->linkPreview(), replyRecipients: $request->replyRecipients(), organization: $organization, source: CommitSource::Web);
$parent = $thread->get($commit, $user);

return response()->json([
Expand Down
6 changes: 5 additions & 1 deletion app/Http/Controllers/ScheduledCommitController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Actions\CreateScheduledCommit;
use App\Actions\DeleteScheduledCommit;
use App\Enums\CommitSource;
use App\Http\Requests\CreateScheduledCommitRequest;
use App\Http\Requests\DeleteScheduledCommitRequest;
use App\Http\Resources\ScheduledCommitResource;
Expand All @@ -30,9 +31,12 @@ public function index(Request $request, #[CurrentUser] User $user, ScheduledComm
]);
}

/**
* Schedules a web commit and marks the session so its queue refreshes.
*/
public function store(CreateScheduledCommitRequest $request, #[CurrentUser] User $user, CreateScheduledCommit $action): RedirectResponse
{
$action->handle($user, $request->string('body')->value(), CarbonImmutable::parse($request->string('scheduled_at')->value())->utc(), $request->images(), $request->imageDescriptions(), $request->poll(), $request->linkPreview(), $request->organization());
$action->handle($user, $request->string('body')->value(), CarbonImmutable::parse($request->string('scheduled_at')->value())->utc(), $request->images(), $request->imageDescriptions(), $request->poll(), $request->linkPreview(), $request->organization(), CommitSource::Web);

Inertia::flash('scheduled', true);
Inertia::flash('toast', ['type' => 'success', 'message' => 'Commit scheduled. Only you can see it until it is submitted.']);
Expand Down
3 changes: 3 additions & 0 deletions app/Http/Resources/CommitResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
final class CommitResource extends JsonResource
{
/**
* Serializes public commits, signed-only fields, and deleted thread tombstones without exposing their body.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
Expand All @@ -56,6 +58,7 @@ public function toArray(Request $request): array
'id' => $this->id,
'short_hash' => mb_substr(str_replace('-', '', $this->id), -7),
'body' => $this->body,
'source' => $this->source?->value,
'parent_id' => $isSigned ? $this->parent_id : $this->parent?->id,
'html_url' => route('commit.show', [$this->author, $this->resource]),
'created_at' => $this->created_at->toIso8601ZuluString(),
Expand Down
3 changes: 3 additions & 0 deletions app/Http/Resources/ScheduledCommitResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
final class ScheduledCommitResource extends JsonResource
{
/**
* Serializes an owner's scheduled commit, withholding its body after submission or deletion.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'author' => $this->author === null ? null : ProfileResource::make($this->author)->resolve($request),
'source' => $this->source?->value,
...($this->submitted_at === null && $this->body !== null
? ['body' => $this->body, ...resolve(CommitBodies::class)->segments($this->body)]
: ['body' => null, 'body_segments' => [], 'preview_segments' => null]),
Expand Down
18 changes: 18 additions & 0 deletions app/Models/Commit.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

namespace App\Models;

use App\Enums\CommitSource;
use App\Enums\CommitStatus;
use App\Queries\CommitBuilder;
use App\Services\CommitBodies;
use App\Services\Mentions;
use Carbon\CarbonInterface;
use Database\Factories\CommitFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Attributes\UseEloquentBuilder;
use Illuminate\Database\Eloquent\Casts\AsVector;
Expand All @@ -31,6 +33,7 @@
* @property-read string|null $root_id
* @property-read float $hot_score
* @property-read string $body
* @property-read CommitSource|null $source
* @property-read array<string, string>|null $mentions
* @property-read array<string, string>|null $organization_mentions
* @property-read CommitStatus $status
Expand Down Expand Up @@ -70,6 +73,20 @@
'embedding',
'headlines_classified_at',
])]
#[Fillable([
'user_id',
'author_id',
'body',
'source',
'mentions',
'parent_id',
'root_id',
'status',
'link_preview_id',
'quoted_commit_id',
'hot_score',
'created_at',
])]
#[UseEloquentBuilder(CommitBuilder::class)]
final class Commit extends Model
{
Expand Down Expand Up @@ -124,6 +141,7 @@ public function casts(): array
'hot_score' => 'float',
'pasteis_count' => 'integer',
'body' => 'string',
'source' => CommitSource::class,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
'mentions' => 'array',
'organization_mentions' => 'array',
'status' => CommitStatus::class,
Expand Down
Loading