diff options
| -rw-r--r-- | src/features/messages.js | 26 | ||||
| -rw-r--r-- | src/style.css | 9 | ||||
| -rw-r--r-- | tools/gif-picker-test.js | 4 | ||||
| -rw-r--r-- | tools/post-actions-test.js | 5 |
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, }; |
