aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-10 08:28:53 -0700
committerAdam Soltys <[email protected]>2026-10-10 08:28:53 -0700
commit42c0ef9c6391285d76825bdf0d29371cd6ebf500 (patch)
treef3e3e09aac010a7fa71da1f3bf2969d0e6d8f91f
parent1573c1d5327ebc941f4308e2b0cc88e85a640add (diff)
downloadcoinosv3-42c0ef9c6391285d76825bdf0d29371cd6ebf500.tar.gz
coinosv3-42c0ef9c6391285d76825bdf0d29371cd6ebf500.zip
Composers: attach and GIF sit in the field's corner. The post composer's row had grown to Post · 📎 · GIF · Preview · Cancel; the reply box's likewise. The paperclip and GIF button are now small borderless tools inside the text field, bottom right (the field keeps room under its text so nothing runs beneath them), and the row under it is only what you do with the post: Post, Preview, Cancel (Preview, Cancel, Reply in a thread). Tests find the tools in .compose-box.
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
-rw-r--r--src/features/messages.js26
-rw-r--r--src/style.css9
-rw-r--r--tools/gif-picker-test.js4
-rw-r--r--tools/post-actions-test.js5
4 files changed, 30 insertions, 14 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 5040b0f..28e20f1 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -9960,6 +9960,7 @@ export function messagesFeature(ctx) {
// 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('div', { class: 'compose-box' },
h('coinos-text', {
class: 'thread-reply-input', placeholder: t('threadReplyHint'),
style: 'font-family:var(--sans);min-height:64px;max-height:160px',
@@ -9990,15 +9991,17 @@ export function messagesFeature(ctx) {
}
},
}),
+ h('div', { class: 'compose-tools' },
+ (ctx.uploadMedia || ctx.uploadImage) ? h('button', {
+ type: 'button', class: 'compose-tool', title: t('feedAttach'), 'aria-label': t('feedAttach'), disabled: !!ui.postUploading,
+ onClick: () => document.getElementById('reply-file')?.click(),
+ }, ui.postUploading ? h('span', { class: 'spinner sm' }) : h('span', { style: 'display:flex', html: CLIP })) : null,
+ gifs.button('reply', 'compose-tool gif-btn'))),
mentionList('.thread-reply-input'),
h('div', { class: 'row reply-tools wrap', 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.
- (ctx.uploadMedia || ctx.uploadImage) ? h('button', {
- class: 'attach-btn', title: t('feedAttach'), 'aria-label': t('feedAttach'), disabled: !!ui.postUploading,
- onClick: () => document.getElementById('reply-file')?.click(),
- }, ui.postUploading ? h('span', { class: 'spinner sm' }) : h('span', { style: 'display:flex', html: CLIP })) : null,
h('input', {
type: 'file', id: 'reply-file', accept: 'image/*,video/*', style: 'display:none',
onChange: async (e) => {
@@ -10012,7 +10015,6 @@ export function messagesFeature(ctx) {
});
},
}),
- gifs.button('reply', 'attach-btn gif-btn'),
h('span', { class: 'grow' }),
h('div', { class: 'row gap6 composer-options' },
previewBtn(!!s.preview, () => { s.preview = !s.preview; render(); }),
@@ -10741,6 +10743,9 @@ export function messagesFeature(ctx) {
if (ui.profCompose == null && !draftFor(POST_DRAFT)) return null;
const text = composeText();
return h('div', { class: 'col', style: 'gap:8px' },
+ // the attach and GIF buttons sit in the field's corner: the row under
+ // it is for what you do with the post (Post, Preview, Cancel)
+ h('div', { class: 'compose-box' },
h('coinos-text', {
class: 'post-input', placeholder: t('profComposePh'),
style: 'font-family:var(--sans);min-height:84px;max-height:320px',
@@ -10759,6 +10764,12 @@ export function messagesFeature(ctx) {
if (ui.postPreview) render();
},
}),
+ h('div', { class: 'compose-tools' },
+ (ctx.uploadMedia || ctx.uploadImage) ? h('button', {
+ type: 'button', class: 'compose-tool', title: t('feedAttach'), 'aria-label': t('feedAttach'), disabled: !!ui.postUploading,
+ onClick: () => document.getElementById('post-file')?.click(),
+ }, ui.postUploading ? h('span', { class: 'spinner sm' }) : h('span', { style: 'display:flex', html: CLIP })) : null,
+ gifs.button('post', 'compose-tool gif-btn'))),
mentionList('.post-input'),
ui.postPreview ? draftPreview(text) : null,
mediaBar(),
@@ -10781,15 +10792,10 @@ export function messagesFeature(ctx) {
render();
}
} }, t('profPostBtn')),
- (ctx.uploadMedia || ctx.uploadImage) ? h('button', {
- class: 'attach-btn', title: t('feedAttach'), 'aria-label': t('feedAttach'), disabled: !!ui.postUploading,
- onClick: () => document.getElementById('post-file')?.click(),
- }, ui.postUploading ? h('span', { class: 'spinner sm' }) : h('span', { style: 'display:flex', html: CLIP })) : null,
h('input', {
type: 'file', id: 'post-file', accept: 'image/*,video/*', style: 'display:none',
onChange: async (e) => { const f = e.target.files && e.target.files[0]; e.target.value = ''; await attachMedia(f); },
}),
- gifs.button('post', 'attach-btn gif-btn'),
h('div', { class: 'row gap6 composer-options' },
previewBtn(!!ui.postPreview, () => { ui.postPreview = !ui.postPreview; render(); }),
h('button', { class: 'btn-ghost btn-sm', onClick: requestCancelPost }, t('cancel')))),
diff --git a/src/style.css b/src/style.css
index f0b5de8..59ead7b 100644
--- a/src/style.css
+++ b/src/style.css
@@ -1652,3 +1652,12 @@ body::after {
.gif-tile video, .gif-tile img { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; }
.gif-note { display: flex; justify-content: center; padding: 32px 0; }
.note-reposted { margin-bottom: 2px; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+/* attach + GIF inside the composer's field, bottom right; the field keeps
+ room under its text so a long post never runs beneath them */
+.compose-box { position: relative; }
+.compose-box > coinos-text { padding-bottom: 42px; }
+.compose-tools { position: absolute; right: 6px; bottom: 6px; display: flex; gap: 2px; }
+.compose-tool { width: 34px; height: 32px; padding: 0; display: inline-flex; align-items: center; justify-content: center;
+ border: 0; border-radius: 8px; background: transparent; color: var(--muted); cursor: pointer; }
+.compose-tool:hover:not(:disabled) { background: var(--line); color: var(--ink); }
+.compose-tool.gif-btn { font-size: 11px; width: 38px; }
diff --git a/tools/gif-picker-test.js b/tools/gif-picker-test.js
index 956b4f7..f72cde3 100644
--- a/tools/gif-picker-test.js
+++ b/tools/gif-picker-test.js
@@ -53,8 +53,8 @@ try {
await page.evaluate(() => document.querySelector('.app-nav-button[aria-label="Feed"]')?.click());
await waitFor(() => !!document.querySelector('.feed-post-fab'), 15000);
await page.evaluate(() => document.querySelector('.feed-post-fab').click());
- check('the post composer has a GIF button', await waitFor(() => !!document.querySelector('.post-actions .gif-btn'), 8000));
- await page.evaluate(() => document.querySelector('.post-actions .gif-btn').click());
+ check('the post composer has a GIF button', await waitFor(() => !!document.querySelector('.compose-box .gif-btn'), 8000));
+ await page.evaluate(() => document.querySelector('.compose-box .gif-btn').click());
check('the picker opens with the nostr.build attribution, linked', await waitFor(() => !!document.querySelector('.gif-sheet .gif-head a[href="https://nostr.build"]'), 5000));
check('...and the first starter\'s GIFs', await waitFor(() => document.querySelectorAll('.gif-grid .gif-tile').length >= 6, 20000), String(await page.evaluate(() => document.querySelectorAll('.gif-tile').length)));
check('a tile plays the small mp4, not the original', await waitFor(() => [...document.querySelectorAll('.gif-tile video')].some((v) => /mp4/.test(v.currentSrc || v.getAttribute('src') || '') && v.readyState >= 2), 15000));
diff --git a/tools/post-actions-test.js b/tools/post-actions-test.js
index a33079e..766cfa3 100644
--- a/tools/post-actions-test.js
+++ b/tools/post-actions-test.js
@@ -93,10 +93,11 @@ try {
const reply = await page.evaluate(() => {
const box = document.querySelector('.thread-reply-input');
if (!box) return { box: false };
- const row = box.parentElement;
+ const row = box.closest('.thread-reply');
return {
box: true,
- attach: !!row.querySelector('.attach-btn'),
+ // in the field's corner now, beside the GIF button
+ attach: !!row.querySelector('.compose-box .compose-tool:not(.gif-btn)'),
file: !!row.querySelector('#reply-file'),
accepts: (row.querySelector('#reply-file') || {}).accept,
};