From a4f13b845b9dfb364bdac0932916d99c381db20e Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 26 Sep 2026 07:50:39 -0700 Subject: Thread: the reply box appears on Reply, full width, with its tools on a row beneath. A thread opened to read came with an empty field under its first post. The box now appears under the note whose Reply was tapped (or while a draft is pending), folds away once the reply is sent or Cancel is tapped, and takes Amethyst's shape: the field across the full width, attach and preview on a row beneath it with Cancel and Reply at the right. Tests: thread-reply's helper follows the new shape and expects the box to fold after sending; compose-preview and feed-list tap Reply before expecting a box (the thread card carries .thread-card as the marker). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VhUKyXKtsJ67QAFfb7QR9D --- src/features/messages.js | 25 ++++++++++++++++++------- src/style.css | 3 ++- 2 files changed, 20 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/features/messages.js b/src/features/messages.js index a44a9b7..357de3f 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -5202,6 +5202,7 @@ export function messagesFeature(ctx) { } else ui.noteThread = { rootId: rootIdOf(ev), focusId: ev.id, seed: ev }; const s = ui.noteThread; ui.profOverThread = false; + s.replying = true; // the box appears under this note s.scrollPending = false; // focus the composer, without a competing note scroll s.refocus = true; render(); @@ -6115,16 +6116,19 @@ export function messagesFeature(ctx) { const row = (ev) => noteRow(ev.pubkey, ev, displayName(ev.pubkey), { open: false, focus: ev.id === s.focusId && ev.id !== c.rootId }); // The reply box sits INLINE, right under the note it answers — it used // to live at the bottom of the thread, where nobody scrolled to find it. - const replyBox = () => h('div', { class: 'col', style: 'gap:8px;padding:2px 0 10px' }, + // The box appears once Reply is tapped (or while a draft is pending) — + // a thread opened to read used to come with an empty field under its + // first post. Amethyst's shape: the field across the full width, the + // tools on a row beneath it. + const replyBox = () => h('div', { class: 'col thread-reply', style: 'gap:8px;padding:2px 0 10px' }, s.preview ? draftPreview(s.draft) : null, - h('div', { class: 'row', style: 'gap:8px;align-items:flex-end' }, // A textarea that grows with the reply, like the chat composer: Enter // sends, Shift+Enter (or Ctrl+J) breaks the line — a reply used to be // a one-line field with no way to write a paragraph. h('textarea', { - class: 'grow thread-reply-input', placeholder: t('threadReplyHint'), - rows: String(Math.min(5, (s.draft || '').split('\n').length)), - style: 'font-family:var(--sans);resize:none;max-height:120px;overflow-y:auto;line-height:1.4', + class: 'thread-reply-input', placeholder: t('threadReplyHint'), + rows: String(Math.min(6, Math.max(2, (s.draft || '').split('\n').length))), + style: 'font-family:var(--sans);resize:none;max-height:160px;overflow-y:auto;line-height:1.4;width:100%;box-sizing:border-box', value: s.draft || '', // a render per keystroke only while the preview is open; the morph // leaves a focused field alone, so this can't fight the typing @@ -6142,6 +6146,7 @@ export function messagesFeature(ctx) { } }, }), + h('div', { class: 'row reply-tools', style: 'gap:8px;align-items:center' }, // A reply can carry a picture too — same upload, same imeta tag, same // paperclip. The URL lands in the draft, which is what every client // reads as the media. @@ -6163,6 +6168,11 @@ export function messagesFeature(ctx) { }, }), previewBtn(!!s.preview, () => { s.preview = !s.preview; render(); }), + h('span', { class: 'grow' }), + h('button', { + class: 'btn-ghost btn-sm thread-reply-cancel', type: 'button', + onClick: () => { s.replying = false; s.preview = false; render(); }, + }, t('cancel')), h('button', { class: 'btn-primary thread-reply-send', disabled: !!s.sending || !!ui.postUploading, onClick: async () => { @@ -6177,6 +6187,7 @@ export function messagesFeature(ctx) { // focused field's value — clear it by hand const inp = document.querySelector('.thread-reply-input'); if (inp) inp.value = ''; + s.replying = false; s.preview = false; // sent: the box folds away toast(t('threadReplied')); } catch (e) { if (!e.silent) toast(e.message); } s.sending = false; render(); @@ -6186,7 +6197,7 @@ export function messagesFeature(ctx) { // nothing narrower is focused; appended at the end if the focused note // hasn't loaded yet, so the box never disappears entirely) const kids = []; - let boxPlaced = false; + let boxPlaced = !(s.replying || (s.draft || '').trim()); // nothing to place until Reply is tapped const place = (ev) => { if (boxPlaced) return; if (ev.id === s.focusId || (!s.focusId && c.root && ev.id === c.root.id)) { @@ -6249,7 +6260,7 @@ export function messagesFeature(ctx) { // full header: search/chat/settings stay reachable mid-thread (only // the public no-wallet surface drops the action row) ctx.brandHeader(!ui.pubProf && wallet.loaded), - h('div', { class: 'card col', style: 'gap:0;padding:2px 14px' }, ...kids), + h('div', { class: 'card col thread-card', style: 'gap:0;padding:2px 14px' }, ...kids), h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.noteThread = null; render(); } }, t('back')), ...noteOverlays()); } diff --git a/src/style.css b/src/style.css index b136432..83e67a0 100644 --- a/src/style.css +++ b/src/style.css @@ -684,7 +684,8 @@ html:has(#app .chat-card), body:has(#app .chat-card) { overflow: hidden; } display: flex; align-items: center; justify-content: center; padding: 0; } /* beside a growing reply box the icons keep their size, sat on the baseline */ -.thread-reply-input ~ .attach-btn { align-self: flex-end; height: 44px; } +.thread-reply .reply-tools .attach-btn { height: 40px; } +.thread-reply .thread-reply-send { padding: 8px 18px; } /* A picture, full screen: the image centred on a dark ground, everything else out of the way. Fixed to the viewport (svh, so a phone's address bar can't push it), and above every other layer including the toast. */ -- cgit v1.2.3