diff --git a/tests/ui/app.spec.js b/tests/ui/app.spec.js index 1baa3a0..d3e8dac 100644 --- a/tests/ui/app.spec.js +++ b/tests/ui/app.spec.js @@ -1671,7 +1671,8 @@ test.describe('an item with no files, on a phone', () => { }); test('a pinned feed, even one from inside a folder, goes to the top of the list', async ({ page }) => { - const rows = page.locator('#feedlist .feed'); + // The Signals folder is first, under All Subscriptions (#256), and no feed. + const rows = page.locator('#feedlist .feed:not(.sigs)'); const group = page.locator('.feed.group').first(); if ((await group.locator('.chev').getAttribute('aria-expanded')) !== 'true') await group.locator('.chev').click(); const child = page.locator('.feed.child').first(); @@ -2078,9 +2079,8 @@ test('a Signal holds the items Jev says fit its keywords, read where they are re const { id: feed } = await (await page.request.post('/api/feeds', { data: { url: 'http://127.0.0.1:8792/signal.xml' } })).json(); await page.reload(); - // A folder, closed until opened, as an OPML's is (#255). - await page.locator('#feedlist .sigs .chev').click(); - await page.locator('#feedlist .feed', { hasText: 'New signal' }).click(); + // The + on the folder's own row (#256); making one opens the folder to show it. + await page.locator('#feedlist .sigs .sigadd').click(); await page.locator('#sgname').fill('F1'); await page.locator('#sgkwin').fill('monaco'); await page.keyboard.press('Enter'); diff --git a/web/CHANGELOG.md b/web/CHANGELOG.md index f8b9d3c..407f150 100644 --- a/web/CHANGELOG.md +++ b/web/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Signals pick what matters out of the noise of every feed you subscribe to: New signal takes a name and what to listen for, and its page lists the items from your feeds that are about it, read and unread as in their own feeds. Settings takes your TypeSafe key, which Signals need (#254). They are kept in a Signals folder under your pinned feeds, closed or open as an OPML is, with its own page listing them (#255). +- Signals pick what matters out of the noise of every feed you subscribe to: New signal takes a name and what to listen for, and its page lists the items from your feeds that are about it, read and unread as in their own feeds. Settings takes your TypeSafe key, which Signals need (#254). They are kept in a Signals folder first in the feed list, under All Subscriptions, closed or open as an OPML is, with a + on its row for a new one and its own page listing them (#255, #256). - A list opens at once when you have seen it before or the page fetched it ahead: All Subscriptions and the feeds with something unread are fetched when the page loads, and a feed or filter as the pointer or a finger reaches it, then refreshed behind once shown. A list that has to be waited for shows a spinner instead of nothing (#247). Currently Listening and the Directory's front page are fetched ahead the same way and open at once (#248). ### Changed diff --git a/web/app.css b/web/app.css index 05dbe74..a36f7d7 100644 --- a/web/app.css +++ b/web/app.css @@ -592,6 +592,9 @@ input:focus,select:focus{outline:0;border-color:var(--accent)} .feed.child .art{width:28px;height:28px;font-size:11px} /* A Signal's tile is its icon, as a place's is: there is no artwork of its own. */ .art.sig .i{width:45%;height:45%} +/* New signal, at the end of the folder's row. */ +.sigadd{width:26px;height:26px;margin-left:-4px} +.sigadd .i{width:12px;height:12px} /* Only a folder has a triangle, hung in the margin so every feed's art lines up with the places above it. The button is the row's full height and 24 px wide: a near miss used to open the folder's page, and 16 px left the triangle cramped against the art. */ diff --git a/web/src/feeds.ts b/web/src/feeds.ts index b295f9b..6586a76 100644 --- a/web/src/feeds.ts +++ b/web/src/feeds.ts @@ -104,8 +104,9 @@ function renderFeeds(){ places.appendChild(el); } list.appendChild(places); + signalFolder(list,q); const shown=S.feeds.filter(f=>!q||(f.title||f.id).toLowerCase().includes(q)); - if(!shown.length){ signalFolder(list,q); list.insertAdjacentHTML('beforeend','

No feeds.

'); return done(); } + if(!shown.length){ list.insertAdjacentHTML('beforeend','

No feeds.

'); return done(); } // Feeds from a subscribed OPML sit under it, so the group reads as one thing. Pinned feeds // go first: a folder with its feeds under it, a feed from inside one lifted out of it. @@ -124,10 +125,7 @@ function renderFeeds(){ // it is open. const lastTop=order.findIndex(([f,d])=>!d&&!f.pinned); const lastPin=(lastTop<0?order.filter(([f])=>f.pinned):order.slice(0,lastTop)).pop()?.[0]; - // The Signals folder goes under the pinned feeds, which are first because you put them there. - let folder=false; for(const [f,depth] of order){ - if(!folder&&!depth&&!f.pinned){ signalFolder(list,q); folder=true; } const kids=inside(f).length; // A subscription holds no entries itself, so its counts are the sum of what is inside -- // taken from every feed it holds, not just the ones a filter left showing. @@ -171,7 +169,6 @@ function renderFeeds(){ for(const img of $$('img',el)){ const was=drawn.get(img.outerHTML)?.shift(); if(was) img.replaceWith(was); } list.appendChild(el); } - if(!folder) signalFolder(list,q); paintScanning(); done(); } diff --git a/web/src/signals.ts b/web/src/signals.ts index a88c200..fd3b3df 100644 --- a/web/src/signals.ts +++ b/web/src/signals.ts @@ -12,11 +12,11 @@ function signalOf(view: string|null){ return id===null ? undefined : S.signals.find(s=>s.id===id); } -/// The Signals folder's place: a folder of their own at the top of the feed list, open or closed as -/// an OPML's is (#255). Under the places, a long list of them pushed the feeds down. +/// The Signals folder: first in the feed list, under All Subscriptions, open or closed as an OPML's +/// is (#255, #256). As rows under the places, a long list of them pushed the feeds down. const SIGNALS=':signals'; // Tinted as a feed's initials are: a plain tile vanished into a selected row (see .art.ini). -const sigArt=(icon=ICON.signal)=>`
${icon}
`; +const sigArt=()=>`
${ICON.signal}
`; function feedRow(id: string, cls: string, html: string, open: ()=>void){ const el=document.createElement('div'); el.className='feed '+cls+(S.feed===id?' sel':''); @@ -33,9 +33,12 @@ function signalFolder(list: HTMLElement, q: string){ list.appendChild(feedRow(SIGNALS,'sigs', ``+ sigArt()+`
Signals${n?plural(n,'signal'):'What matters, out of the noise'}
`+ - badgeHTML(S.signals.reduce((k,s)=>k+s.unread,0)), + badgeHTML(S.signals.reduce((k,s)=>k+s.unread,0))+ + ``, ()=>selectFeed(SIGNALS))); - $('.chev',list.lastElementChild!).onclick=(ev: Event)=>{ ev.stopPropagation(); toggleGroup(SIGNALS); }; + const row=list.lastElementChild!; + $('.chev',row).onclick=(ev: Event)=>{ ev.stopPropagation(); toggleGroup(SIGNALS); }; + $('.sigadd',row).onclick=(ev: Event)=>{ ev.stopPropagation(); signalModal(); nav(false); }; if(!open) return; for(const sg of mine){ const el=feedRow(':signal:'+sg.id,'child', @@ -44,7 +47,6 @@ function signalFolder(list: HTMLElement, q: string){ el.onpointerenter=el.ontouchstart=()=>prefetch(':signal:'+sg.id); list.appendChild(el); } - list.appendChild(feedRow(':signal:new','child',sigArt(ICON.plus)+'
New signal
',()=>signalModal())); } /// The folder's page: every Signal, as an OPML's page lists its feeds. @@ -136,6 +138,8 @@ function signalModal(sg?: Signal){ const r=sg ? await api(`/api/signals/${sg.id}`,{method:'PATCH',body}) : await api('/api/signals',{method:'POST',body}); closeModal(); toast(sg?'Saved. Jev reads everything again':'Listening. What Jev picks out shows up here'); + // Open, so the new one is there to see. + if(!expanded.has(SIGNALS)) toggleGroup(SIGNALS); await loadFeeds(true); selectFeed(':signal:'+(sg?sg.id:r.id)); }catch(e){ toast(why(e),true); } };