Repository source: docs/plans/archive/forte_reply_step4_implementation_summary.md
Forte Reply — Step 4: Implementation Summary
Stage 1 - Embed hidden compose panel markup
Changes:
Added
templates/partials/paned_compose_panel.php: wrapspartials/reply_form.phpin ahidden.paned-compose-panelcontainer, prefilled withthread_id/parent_id= the thread's root post,board_tags=general, using a Forte-specificpaned-compose-formclass (not the classic.stack) to keep styling isolated fromsite.css.Included the new partial from
templates/partials/paned_content_pane.php, passing through$thread(already available in that scope).
Verification:
php -lclean on both new/changed files.curl http://127.0.0.1:8001/threads/root-001/forte— 200 OK; response contains<div class="paned-compose-panel" data-paned-compose-panel hidden>with apaned-compose-formreusing/compose/reply, and hidden inputsthread_id=root-001/parent_id=root-001.curl http://127.0.0.1:8001/threads/root-001(classic page) — 200 OK;grep -c paned-compose-panelreturns 0, confirming no leakage into the classic UI.Toolbar "Reply" button remains
disabled(unchanged) since wiring it up is Stage 2's scope.
Notes:
reply_form.phpwas reused completely unchanged; only its container andformClassdiffer per Step 2's shared-component inventory.
Stage 2 - Enable Reply button and wire panel toggle
Changes:
templates/pages/forte.php: removeddisabledfrom the toolbar Reply button, addeddata-paned-replyhook (button is unconditionally enabled since Forte always has a default-selected post).public/assets/paned_reader.js: added a click handler on[data-paned-reply]that toggles[data-paned-compose-panel]'shiddenattribute; follows the samequerySelector+ guardedaddEventListenerpattern already used for the prev/next buttons.
Verification:
node --check public/assets/paned_reader.jsandphp -l templates/pages/forte.phpboth clean.curl http://127.0.0.1:8001/threads/root-001/forte— Reply button now renders withoutdisabledand withdata-paned-reply; New/Refresh buttons remaindisabled(untouched, out of scope).Headless-browser click test (Selenium + system
chromium-browser/chromedriver, no new project deps): loaded/threads/root-001/forte, confirmed panel startshidden, first click on Reply removeshidden, second click restores it, zeroSEVEREconsole log entries.
Notes:
Selection-based sync of the panel's
parent_idis deferred to Stage 3, as planned.
Stage 3 - Sync compose panel target with pane selection
Changes:
public/assets/paned_reader.js: hoisted thecomposePanellookup to the top of theDOMContentLoadedhandler (alongsidelistBody/contentPane) and had the centralselectPost(postId)function update the compose form's hiddeninput[name="parent_id"]on every selection change (click, prev/next buttons, and arrow-key navigation all funnel throughselectPost, so all of them stay in sync automatically). Removed the now-redundantcomposePanellookup from the Stage 2 toggle-wiring block.Per the Stage 3 risk note, only the hidden
parent_idfield is reset on selection change; any in-progress body text the reader typed is left untouched.
Verification:
node --check public/assets/paned_reader.jsclean.Headless-browser test (Selenium +
chromium-browser): on/threads/root-001/forte,parent_idstarts asroot-001; clicking thereply-001row updates it toreply-001; clicking back to theroot-001row restoresroot-001; zeroSEVEREconsole log entries.
Notes:
No change needed to the prev/next or arrow-key handlers since they all call the same
selectPost.
Stage 4 - Style compose panel to match Forte chrome
Changes:
public/assets/forte.css: added scoped rules for.paned-compose-panel/.paned-compose-head(beveled panel matching.paned-content-head's look),.paned-compose-form textarea(monospace body font, sunken-bevel border matching.paned-content-pane), and.paned-window .paned-compose-form button(raised-bevel toolbar-style buttons). Nosite.csschanges.Fixed a real cross-stylesheet leak found during visual verification:
site.css's.compose-form-actions > button[type="submit"] { flex: 1 1 auto }(from the classic compose UI) was stretching the reused submit buttons full-width inside the Forte panel since the new rules hadn't overriddenflex. Added.paned-window .paned-compose-form .compose-form-actions > button { flex: 0 0 auto; width: auto; }(higher specificity than the site.css rule) so the buttons render at their natural, toolbar-like width instead.
Verification:
grep -c paned-compose public/assets/site.css— 0 (no leakage into the classic stylesheet).Headless-browser screenshots (Selenium +
chromium-browser) of/threads/root-001/fortewith the composer open: panel head, textarea, and buttons all render in the paned beveled/monospace style consistent with the rest of the window; buttons are compact and toolbar-like rather than the classic full-width bars, after the flex fix above.git status --short public/assets/confirms onlyforte.csschanged among stylesheets.
Notes:
This stage is a good illustration of Step 2's stated maintenance risk: reusing
reply_form.php's classes across two independently-styled surfaces means a classic-side layout rule can leak into Forte until explicitly overridden.
Stage 5 - Return to Forte after replying
Changes:
partials/reply_form.php: added a hiddenreturn_toinput, defaulting to''when the caller doesn't pass one (classic callers are unchanged).partials/paned_compose_panel.php: passesreturnTo=/threads/{root_post_id}/forte.Application.php::handleComposeReplySubmit: on success, resolves the redirect target via newresolveComposeReplyReturnTo(string $requestedReturnTo, string $threadId): string, which only honorsreturn_toif it exactly matches/threads/{the actual created reply's thread_id}/forte; anything else (empty, a foreign URL, or a mismatched thread id) falls back to the original/threads/{thread_id}classic location. This closes an open-redirect/header-injection risk from trusting a client-supplied hidden field outright.Validation-error path (
renderComposeReplyPage) intentionally still renders the classic compose-error page rather than a Forte-native one — building that view is materially larger scope (a new template) than this stage's budget, and the common/success path now stays in Forte. Flagged as a known follow-up rather than silently expanded.
Verification:
php -lclean on all three changed files.curl -i -X POST /compose/replywith a validreturn_to=/threads/root-001/forte—303redirectLocationcorrectly targets/threads/root-001/forte?created_post_id=...#post-....Same request with no
return_to— redirects to the original classic/threads/root-001?...location (unchanged behavior).Same request with
return_to=https://evil.example.comand withreturn_topointing at a different thread's Forte URL — both fall back to the classic/threads/root-001?...location (open-redirect/cross-thread guard confirmed).curl http://127.0.0.1:8001/threads/root-001/forteafter a successful post-from-Forte submission shows the new reply body text in the rendered page.
Notes:
No changes needed to
post_card.php/thread_root_card.php's classic "Reply" links orthread.php's inline composer — they don't passreturnTo, so they keep the pre-existing classic redirect behavior automatically.