Repository source: docs/plans/thread_inline_reply_form_plan_v1.md
Thread Inline Reply Form Plan V1
Goal
Add a compact reply composer directly on thread pages so a reader can start replying without leaving the thread. The composer should render small at rest, expand into the full reply form when clicked or focused, and submit through the existing signed compose reply flow.
Current State
Thread pages render the thread header and post cards in
templates/pages/thread.php.Each post card has a
Replylink to/compose/reply?thread_id=...&parent_id=...fromtemplates/partials/post_card.php.The full reply form lives in
templates/pages/compose_reply.php.Reply submission is handled by
Application::handleComposeReplySubmit()and redirects back to the thread withcreated_post_id.Thread pages already load
/assets/openpgp.min.jsand/assets/browser_signing.js, so inline compose markup can reuse the existing identity, signing, normalization, and draft code.browser_signing.jsbinds the first[data-compose-root]and its[data-compose-form], so the first implementation should keep one inline composer per thread page unless that binding is generalized.
Product Behavior
Show one compact "Reply to thread" composer near the bottom of the thread after the existing posts.
The compact state should take little vertical space: a single-line prompt or short textarea-like control plus no advanced fields.
Clicking, focusing, or keyboard-opening the compact composer expands it to the full reply form.
The expanded form should include the same functional fields as
/compose/reply: hiddenthread_id, hiddenparent_id, hiddenauthor_identity_id, board tags, body, normalization status, identity status, and submit button.The inline reply should default to replying to the thread root post. Existing per-post
Replylinks remain available for replying to a specific post.After successful submission, the existing redirect should return to
/threads/{thread_id}?created_post_id=....Without JavaScript, the user should still be able to reach a working reply path. Either keep the existing
/compose/replylink visible in the compact form or use nativedetailsso expansion is possible without custom JS.
Implementation Plan
Stage 1 - Extract Shared Reply Form Markup
Create a reusable reply form partial, likely
templates/partials/reply_form.php.Move the field markup from
templates/pages/compose_reply.phpinto the partial without changing field names ordata-compose-*attributes.Keep page-specific wrapper content in
compose_reply.php, including the title, feedback partial, and thread/parent ID display.Parameters should cover
threadId,parentId,boardTags,body, submit label, and an optional compact/inline class.Verification:
/compose/reply?thread_id=root-001&parent_id=root-001still renders the same required fields.Existing compose normalization smoke tests still pass.
Stage 2 - Render Compact Inline Composer On Thread Pages
Add a reply composer card after the rendered posts in
templates/pages/thread.php.Use the shared reply form partial with
threadId = $thread['root_post_id']andparentId = $thread['root_post_id'].Wrap it in a single
[data-compose-root]sobrowser_signing.jscan bind existing identity and draft behavior.Use a compact container class such as
inline-reply-composerand an expanded-state class/attribute.Include a plain link to
/compose/reply?thread_id=...&parent_id=...as fallback or secondary action.Keep existing post-card reply links unchanged.
Verification:
Thread smoke test asserts the inline composer exists with root thread hidden IDs.
Existing post-card reply links are still present.
Stage 3 - Add Expansion Behavior And Styling
Add CSS in
public/assets/site.cssfor the compact and expanded composer states.Prefer stable dimensions: compact state should not cause large layout shifts around the post list, and expanded state should match the existing form spacing.
Add small JS only if native
detailsis not sufficient. If JS is needed, create a focused asset such as/assets/inline_reply_form.js.Expansion triggers:
click or focus inside the compact control
keyboard activation on the compact trigger
draft restoration with existing body text should start expanded so saved text is visible
Avoid hiding required form fields from assistive technologies in a way that blocks completion after expansion.
Verification:
Manual browser check: thread page loads compact, expands on click/focus, accepts typing, and submits.
Check mobile width for no overlapping controls or clipped button text.
Stage 4 - Tests And Static Artifacts
Extend
LocalAppSmokeTest::testApplicationRendersCoreRoutesfor the inline reply composer.Add or extend browser-signing JS tests if expansion behavior touches
browser_signing.js.If a new JS asset is introduced, confirm it is included only on thread pages and receives the versioned asset path from the layout.
Run
php tests/run.php.If static artifact generation depends on anonymous thread rendering, confirm the new inline form and asset references appear in generated thread artifacts.
Design Notes
Reuse the existing compose POST endpoint. Do not add an API or database change.
Keep one inline composer for the first version. Multiple inline forms would require generalizing
browser_signing.jsfromquerySelector("[data-compose-root]")to multiple compose roots and resolving draft-key collisions carefully.The inline composer should feel like part of the thread page, not a modal. A compact card with an expanding body is enough.
The
/compose/replypage remains useful for direct links, no-JS fallback, and specific parent replies from individual posts.
Risks
Duplicating reply form markup could let inline and standalone compose behavior drift; extracting a partial reduces this risk.
If the compact form renders hidden fields incorrectly, draft restore or signing could fail because
browser_signing.jsdepends on field names anddata-compose-kind="reply".If expansion uses only JavaScript, no-JS users could lose the inline path; keep a direct compose link or native expansion fallback.
Adding multiple compose roots on a page without updating
browser_signing.jswould bind only one form.
Implementation Progress
Stage 1 - Extract Shared Reply Form Markup
Status: complete.
Changes:
Added
templates/partials/reply_form.phpfor the shared signed reply form fields.Updated
templates/pages/compose_reply.phpto render the shared partial while preserving the existing page wrapper and identity status placement.
Verification:
php tests/run.php LocalAppSmokeTest BrowserSigningNormalizationTestpassed.
Stage 2 - Render Compact Inline Composer On Thread Pages
Status: complete.
Changes:
Added a single inline reply composer at the bottom of
templates/pages/thread.php.The inline composer uses the shared reply form partial and defaults to replying to the thread root post.
Existing per-post reply links remain unchanged, and the inline composer includes a direct full-page compose fallback link.
Verification:
php tests/run.phppassed.Stage 4 will add explicit smoke coverage for the inline composer.
Stage 3 - Add Expansion Behavior And Styling
Status: complete.
Changes:
Converted the inline composer to a native expandable
detailsblock so the thread page starts with a compact reply prompt.Added compact and expanded composer styles in
public/assets/site.css.Added
public/assets/inline_reply_form.jsand loaded it on thread pages afterbrowser_signing.js; it opens the composer when restored draft body text is present.
Verification:
php tests/run.phppassed.
Stage 4 - Tests And Static Artifacts
Status: complete.
Changes:
Extended
LocalAppSmokeTest::testApplicationRendersCoreRoutesto assert the inline composer markup, signed reply form fields, submit label, and thread-only inline reply asset.Confirmed
/compose/replydoes not load the inline reply asset.Extended static artifact coverage to assert generated thread HTML includes the inline reply asset and composer markup.
Verification:
php tests/run.phppassed.
Follow-Up - Textarea Prompt And Hidden Reply Tags
Status: complete.
Changes:
Replaced the collapsed inline reply summary with an unnamed readonly textarea prompt that invites composing and opens the full inline form.
Removed the visible board tags field from shared reply forms while keeping a hidden
board_tagsvalue for the existing write contract.Updated inline reply JS to focus the real body textarea after opening from the collapsed prompt.
Updated smoke and browser-signing tests for hidden reply tags and the new textarea prompt.
Verification:
php tests/run.phppassed.
Follow-Up - Expanded Form Visual Cleanup
Status: complete.
Changes:
Explicitly hide the collapsed textarea prompt after the inline reply composer opens.
Hide the inline reply identity status while it is only the default
Ready.message; it remains available for active status updates.Reduced the visual weight of the full-page reply fallback link.
Verification:
php tests/run.phppassed.
Follow-Up - Minimal Expanded Inline Form
Status: complete.
Changes:
Removed the visible
Reply to threadheading from the expanded inline composer.Removed the visible
Bodylabel from the expanded inline composer while keeping the textarea accessible witharia-label.Removed the
Open full reply pagefallback link from the expanded inline composer.
Verification:
php tests/run.phppassed.