Skip to content
Merged
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: 1 addition & 1 deletion KNOWLEDGE_BASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,7 @@ Posts awaiting automated checks stay pending if those checks fail; a failed chec

Before you first commit, reply, quote, or schedule a commit, the web composer shows the commit rules, and keeps showing them on every attempt until you send with "Got it, commit": no engagement bait, no low-effort posts, no AI-generated posts, no spam or self-promotion, and no harassment, hate, or private information, followed by a link to the terms. Your first submission must come from the web so you can accept these rules; terminal, GitHub Action, and app posting become available afterwards.

During a **timeout**, you can still read and react, but cannot commit, reply, quote, schedule, send remotes, or start, rename, add people to, or pin messages in orgs from any surface; each attempt says how long is left and why. Scheduled commits wait until the timeout ends. Like bans, timeouts belong to the GitHub account: deleting the /dev/null account and signing in again does not end a timeout or erase its history.
During a **timeout**, you can still read and react, but cannot commit, reply, quote, schedule, open issues, send remotes, or start, rename, add people to, or pin messages in orgs from any surface; each attempt says how long is left and why. Scheduled commits wait until the timeout ends. Like bans, timeouts belong to the GitHub account: deleting the /dev/null account and signing in again does not end a timeout or erase its history.

A **ban** sends you to a page explaining why, where you can only sign out or delete your account; the app refuses everything but deleting the account, and the terminal refuses posting. A ban belongs to the GitHub account, so deleting the /dev/null account and signing in again does not lift it. Banning someone cancels their scheduled commits, so lifting the ban later does not publish them all at once.

Expand Down
17 changes: 12 additions & 5 deletions resources/js/components/issue-command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { KeyboardEvent } from 'react';
import { useState } from 'react';
import { toast } from 'sonner';
import { store } from '@/actions/App/Http/Controllers/IssueController';
import { showTimeout, timeoutFrom } from '@/components/timeout-dialog';
import { WordmarkText } from '@/components/wordmark-text';
import { collectEnvironment } from '@/lib/environment';
import type { IssueEnvironment } from '@/lib/environment';
Expand Down Expand Up @@ -96,11 +97,17 @@ export function useIssueCommand({
);
},
onHttpException: (response) => {
toast.error(
response.status === 429
? 'Issue limit reached: 10 a day. Try again tomorrow.'
: 'GitHub did not confirm the issue. Check upstream before retrying. Your draft is safe.',
);
const timeout = timeoutFrom(response.status, response.data);

if (timeout) {
showTimeout(timeout);
} else {
toast.error(
response.status === 429
? 'Issue limit reached: 10 a day. Try again tomorrow.'
: 'GitHub did not confirm the issue. Check upstream before retrying. Your draft is safe.',
);
}
},
onNetworkError: () => {
toast.error(
Expand Down
2 changes: 1 addition & 1 deletion routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@

Route::middleware('auth')->group(function (): void {
Route::post('issues', [IssueController::class, 'store'])
->middleware('throttle:10,1')
->middleware([PreventPostingInTimeout::class, 'throttle:10,1'])
->name('issue.store');

// Session...
Expand Down
10 changes: 10 additions & 0 deletions tests/Feature/Controllers/UserTimeoutControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,16 @@
->and($message->fresh()?->pinned_at)->toBeNull();
});

it('stops a user in timeout from opening issues', function (): void {
$user = User::factory()->create();
UserTimeout::factory()->for($user)->create();

$this->actingAs($user)
->postJson(route('issue.store'), ['command' => '/bug', 'body' => 'buy my course'])
->assertForbidden()
->assertJsonPath('timeout.reason', 'Spam or self-promotion');
});

it('tells the user how long is left of the timeout that ends last, rounded up', function (string $endsIn, string $remaining): void {
$user = User::factory()->create();
UserTimeout::factory()->for($user)->create(['ends_at' => now()->addMinutes(30)]);
Expand Down