aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/features/messages.js40
-rw-r--r--src/i18n.js1
-rw-r--r--tools/compose-preview-test.js2
-rw-r--r--tools/feed-custom-test.js5
4 files changed, 31 insertions, 17 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 5bdd7a6..67d63c2 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -7455,7 +7455,25 @@ export function messagesFeature(ctx) {
// The sheets a post (or a person) can open: the ⋯ menu, the reaction
// picker, the list picker. Every screen that shows a post draws them —
// the thread and profile pages once left the ⋯ tap doing nothing.
- const noteOverlays = () => [noteSheet(), reactPicker(), listPickSheet(), reportSheet(), signinSheet()];
+ const noteOverlays = () => [noteSheet(), reactPicker(), listPickSheet(), reportSheet(), signinSheet(), feedMenuSheet()];
+ // The feed's own menu, behind the ⋯ in its header.
+ function feedMenuSheet() {
+ if (!ui.feedMenu) return null;
+ const def = feedDef();
+ const close = () => { ui.feedMenu = null; render(); };
+ if (!def) { ui.feedMenu = null; return null; }
+ const visitor = isVisitor();
+ const item = (icon, label, onClick) => h('button', { class: 'btn-block', style: 'text-align:left', onClick: () => { close(); onClick(); } }, icon + ' ' + label);
+ return h('div', { class: 'confirm-pop-backdrop', onClick: (e) => { if (e.target === e.currentTarget) close(); } },
+ h('div', { class: 'card col msg-sheet' },
+ feedPath(def, { share: true }) ? item('\u{1F517}', t('feedShare'), () => shareFeed(def)) : null,
+ item('\u{1F4E1}', t('feedRelays') + (feedRelays(def) ? ' \u00b7 ' + feedRelays(def).length : ''), () => { ui.feedRelayEdit = { id: def.id, input: '' }; }),
+ def.builtin || visitor ? null
+ : adhocFeeds.has(def.id)
+ ? item('\u{1F4BE}', t('feedSaveAdhoc'), () => openFeedEditor(def))
+ : item('\u270e', t('feedEdit'), () => openFeedEditor(def)),
+ h('button', { class: 'btn-ghost btn-block', onClick: close }, t('back'))));
+ }
// ---- a visitor at the action bar ---------------------------------------
// Reading is free; zapping, replying, reacting and following take a
// wallet. A visitor who taps one gets the sign-in doors right there, and
@@ -7551,22 +7569,16 @@ export function messagesFeature(ctx) {
def.of ? avatar(def.of, 'chat-avatar mini', true) : null,
h('h3', { style: 'margin:0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap' },
def.builtin && !def.all ? t('feedTitle') : def.of ? t('feedOfTitle', { name: displayName(def.of) }) : def.name),
- def.builtin || visitor ? null
- : adhocFeeds.has(def.id)
- ? h('button', { class: 'btn-sm', onClick: () => openFeedEditor(def) }, t('feedSaveAdhoc'))
- : h('button', { class: 'btn-sm', title: t('feedEdit'), 'aria-label': t('feedEdit'), onClick: () => openFeedEditor(def) }, '\u270e'),
+ // one ⋯ for the feed itself (share, relays, save or edit) and one
+ // pencil for a new post: the title keeps the room a phone has
h('button', {
- class: 'btn-sm' + (feedRelays(def) ? ' on' : ''), title: t('feedRelays'), 'aria-label': t('feedRelays'), style: 'margin-left:auto;flex-shrink:0',
- onClick: () => { ui.feedRelayEdit = ui.feedRelayEdit && ui.feedRelayEdit.id === def.id ? null : { id: def.id, input: '' }; render(); },
- html: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block"><circle cx="12" cy="12" r="2"/><path d="M16.2 7.8a6 6 0 0 1 0 8.4M7.8 16.2a6 6 0 0 1 0-8.4M19 5a10 10 0 0 1 0 14M5 19A10 10 0 0 1 5 5"/></svg>' }),
- feedPath(def, { share: true })
- ? h('button', { class: 'btn-sm', title: t('feedShare'), 'aria-label': t('feedShare'), style: 'flex-shrink:0', onClick: () => shareFeed(def),
- html: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.6" y1="13.5" x2="15.4" y2="17.5"/><line x1="15.4" y1="6.5" x2="8.6" y2="10.5"/></svg>' })
- : null,
+ class: 'btn-sm' + (feedRelays(def) ? ' on' : ''), title: t('feedMenu'), 'aria-label': t('feedMenu'), style: 'margin-left:auto;flex-shrink:0',
+ onClick: () => { ui.feedMenu = true; render(); },
+ }, '\u22ef'),
visitor ? null : h('button', {
- class: 'btn-sm', style: 'flex-shrink:0',
+ class: 'btn-sm', style: 'flex-shrink:0', title: t('profNewPost'), 'aria-label': t('profNewPost'),
onClick: () => { ui.profCompose = ui.profCompose == null ? (draftFor(POST_DRAFT) || '') : null; render(); },
- }, t('profNewPost'))),
+ html: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>' })),
ui.feedRelayEdit && ui.feedRelayEdit.id === def.id ? relayPanel(def) : null,
visitor ? null : feedChips(),
visitor ? null : postComposer(),
diff --git a/src/i18n.js b/src/i18n.js
index 0f11853..eff5a73 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -247,6 +247,7 @@ const STR = {
feedOfLoading: 'Reading who they follow…',
feedNoFollowsOf: 'They follow nobody yet.',
feedShare: 'Share this feed',
+ feedMenu: 'Feed options',
feedSharedName: 'Shared feed',
feedPackName: 'Follow pack',
profTheirFeed: 'Their timeline',
diff --git a/tools/compose-preview-test.js b/tools/compose-preview-test.js
index d33dcdc..c2f35e6 100644
--- a/tools/compose-preview-test.js
+++ b/tools/compose-preview-test.js
@@ -65,7 +65,7 @@ try {
await sleep(1000);
await page.evaluate(() => { const e = [...document.querySelectorAll('.item')].find((n) => /feed/i.test(n.textContent)); if (e) e.click(); });
await sleep(2500);
- const opened = await click('new post');
+ const opened = await page.evaluate(() => { const b = [...document.querySelectorAll('button')].find((n) => (n.getAttribute('aria-label') || n.textContent).trim().toLowerCase() === 'new post'); if (b) { b.click(); return true; } return false; });
check('the composer opens', opened && !!(await page.$('textarea')));
await type('textarea', DRAFT);
diff --git a/tools/feed-custom-test.js b/tools/feed-custom-test.js
index 4f9f6cb..039b166 100644
--- a/tools/feed-custom-test.js
+++ b/tools/feed-custom-test.js
@@ -130,7 +130,7 @@ try {
await page.evaluate(() => [...document.querySelectorAll('.feed-chip')].find((c) => c.textContent.trim() === 'Following').click()); await sleep(1200);
check('Following again, with its own posts', (await chips())[0] === '*Following' && await waitText('plain post', 4000));
await page.evaluate(() => { const a = [...document.querySelectorAll('.notes-feed a')].find((x) => x.textContent === '#bitcoin'); if (a) a.click(); }); await sleep(1200);
- check('a #tag opens a topic feed of its own', JSON.stringify(await chips()) === JSON.stringify(['Following', 'Popular', '#gardenstr', 'Coin talk', '*#bitcoin', '+']) && await page.evaluate(() => !![...document.querySelectorAll('button')].find((b) => /save feed/i.test(b.textContent))), JSON.stringify(await chips()));
+ check('a #tag opens a topic feed of its own', JSON.stringify(await chips()) === JSON.stringify(['Following', 'Popular', '#gardenstr', 'Coin talk', '*#bitcoin', '+']) && await page.evaluate(() => { const m = document.querySelector('[aria-label="Feed options"]'); if (!m) return false; m.click(); return true; }) && await waitText('save feed', 2000) && await page.evaluate(() => { const b = [...document.querySelectorAll('.msg-sheet button')].find((x) => /back/i.test(x.textContent)); if (b) b.click(); return true; }), JSON.stringify(await chips()));
const treq = (await page.evaluate(() => window.__reqs)).filter((f) => f['#t'] && !f.authors).pop();
check('...asked for by topic alone', !!treq && JSON.stringify(treq['#t']) === JSON.stringify(['bitcoin']), JSON.stringify(treq || null));
@@ -163,7 +163,8 @@ try {
check('the home list shows the pack feed with its pack', await page.evaluate(() => { const b = document.querySelector('.chat-back'); if (b) b.click(); return !!b; }) && await waitText('test pack', 3000));
await page.evaluate(() => { const e = [...document.querySelectorAll('.item')].find((n) => (n.querySelector('.chat-name') || {}).textContent === 'Feed'); if (e) e.click(); }); await sleep(800);
await page.evaluate(() => [...document.querySelectorAll('.feed-chip')].find((c) => c.textContent.trim() === 'Coin talk').click()); await sleep(800);
- await page.evaluate(() => [...document.querySelectorAll('button')].find((b) => /edit feed/i.test(b.getAttribute('aria-label') || '')).click()); await sleep(400);
+ await page.click('[aria-label="Feed options"]'); await sleep(300); // the editor lives in the feed's ⋯ menu now
+ await page.evaluate(() => [...document.querySelectorAll('.msg-sheet button')].find((b) => /edit feed/i.test(b.textContent)).click()); await sleep(400);
check('the editor shows what was saved', await page.evaluate(() => document.querySelector('input[placeholder="#bitcoin #nostr"]').value === '#bitcoin #lightning' && document.querySelector('input[type=checkbox]').checked));
await click('delete feed'); await sleep(1000);
check('deleting it lands back on Following', JSON.stringify(await chips()) === JSON.stringify(['*Following', 'Popular', '#gardenstr', 'Pack feed', '+']), JSON.stringify(await chips()));