diff --git a/KNOWLEDGE_BASE.md b/KNOWLEDGE_BASE.md index 4f575f267..c0b15c83f 100644 --- a/KNOWLEDGE_BASE.md +++ b/KNOWLEDGE_BASE.md @@ -196,7 +196,7 @@ Livestream history: the team agreed to build semantic search on [day two at 2:57 ### Writing commits -A **commit** is a post on /dev/null, not a Git commit in a repository. Open the composer or press **c**, write your update, and choose **Commit** or press **⌘/Ctrl+Enter**. Text-only commits accept 1–4,000 characters; as you type, the ring around your avatar in the composer fills toward that limit. On the web and in the app, an image attachment makes the text optional (terminal commits still need text), but the 4,000-character maximum still applies. Runs of blank lines outside code collapse to one before the length is checked. Lists fold posts longer than 280 characters or 8 lines behind **Show more**, without cutting through code. Commits can be created and deleted, but not edited after publication. There are no plans to add commit editing, and it will never be added: to change a commit, delete it and post a new one. +A **commit** is a post on /dev/null, not a Git commit in a repository. Open the composer or press **c**, write your update, and choose **Commit** or press **⌘/Ctrl+Enter**. Text-only commits accept 1–4,000 characters; as you type, the ring around your avatar in the composer fills toward that limit. On the web and in the app, an image attachment makes the text optional (terminal commits still need text), but the 4,000-character maximum still applies. Runs of blank lines outside code collapse to one before the length is checked. Lists fold posts longer than 280 characters or 8 lines behind **Show more**. When a commit starts with a code block too long to fit, its first lines fade out to show that the code continues. **Show more** reveals the full snippet, and the copy control copies the whole block even while folded. Commits can be created and deleted, but not edited after publication. There are no plans to add commit editing, and it will never be added: to change a commit, delete it and post a new one. Commits, replies, quotes, scheduling, terminal submissions, and app (signed API) commits share a limit of 10 attempts per minute per user. Posts go through [moderation](#moderation), so a newly submitted commit may be pending rather than public immediately. diff --git a/resources/js/components/code-block.tsx b/resources/js/components/code-block.tsx index e355c78f2..210345235 100644 --- a/resources/js/components/code-block.tsx +++ b/resources/js/components/code-block.tsx @@ -62,7 +62,14 @@ export function CodeBlock({ }; return ( -
+
{(language || isCopyable) && (
)} - {/* The server escapes the code while highlighting it. */} -
+            
+ {/* The server escapes the code while highlighting it. */} +
+            
); } diff --git a/resources/js/components/commit-body.tsx b/resources/js/components/commit-body.tsx index d90cbae2d..a43354dc4 100644 --- a/resources/js/components/commit-body.tsx +++ b/resources/js/components/commit-body.tsx @@ -193,11 +193,22 @@ function withFullBlocks( const blocks = segments.filter((segment) => segment.type === 'block'); let index = 0; - return previewSegments.map((segment) => + const preview = previewSegments.map((segment) => segment.type === 'block' ? { ...segment, fullHtml: blocks[index++]?.html } : segment, ); + const last = preview.at(-1); + const previous = preview.at(-2); + + // A faded block already shows that it continues; keep the ellipsis for completed blocks and prose. + return last?.type === 'text' && + last.text.trim() === '…' && + previous?.type === 'block' && + previous.fullHtml !== undefined && + previous.fullHtml !== previous.html + ? preview.slice(0, -1) + : preview; } export function CommitBodyExcerpt({ diff --git a/tests/Unit/FoldedCommitBody.test.tsx b/tests/Unit/FoldedCommitBody.test.tsx new file mode 100644 index 000000000..bd001cd9d --- /dev/null +++ b/tests/Unit/FoldedCommitBody.test.tsx @@ -0,0 +1,75 @@ +import assert from 'node:assert/strict'; +import { App } from '@inertiajs/react'; +import type { ComponentProps } from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { test } from 'vite-plus/test'; +import { FoldedCommitBody } from '../../resources/js/components/commit-body'; +import type { BodySegment } from '../../resources/js/types'; + +function renderBody( + segments: BodySegment[], + previewSegments: BodySegment[] | null, +) { + return renderToStaticMarkup( + ( + + )} + initialPage={ + { + component: 'Preview', + url: '/', + version: null, + props: { + errors: {}, + auth: { user: null, pasteis: null, isAdmin: false }, + }, + clearHistory: false, + encryptHistory: false, + } as unknown as ComponentProps['initialPage'] + } + />, + ); +} + +const block = (html: string): BodySegment => ({ + type: 'block', + language: 'bash', + filename: null, + html, +}); + +test('a shortened code preview uses its continuation cue without a loose ellipsis', () => { + const markup = renderBody( + [block('echo before\necho after')], + [block('echo before'), { type: 'text', text: '\n…' }], + ); + assert.ok(markup.includes('mask-b-from-')); + assert.ok(markup.includes('echo before')); + assert.ok(!markup.includes('echo after')); + assert.ok(!markup.includes('…')); + assert.ok(markup.includes('Show more')); + assert.ok(markup.includes('aria-label="Copy code"')); +}); + +test('a completed code block keeps the ellipsis when later prose is hidden', () => { + const markup = renderBody( + [block('echo ready'), { type: 'text', text: '\nMore explanation' }], + [block('echo ready'), { type: 'text', text: '\n…' }], + ); + assert.ok(!markup.includes('mask-b-from-')); + assert.ok(markup.includes('echo ready')); + assert.ok(markup.includes('…')); + assert.ok(markup.includes('Show more')); +}); + +test('a complete body shows all its code without an expansion control', () => { + const markup = renderBody([block('echo before\necho after')], null); + assert.ok(!markup.includes('mask-b-from-')); + assert.ok(markup.includes('echo after')); + assert.ok(!markup.includes('Show more')); +});