aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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,
};