Popular is a top 10; a Directory lists every listed feed A to Z

- GET /api/popular returns the ten most subscribed feeds. The new GET
  /api/directory returns every feed that may be listed, sorted by name,
  from the same list: everyone counted, you included, never a URL, never
  a private feed or a feed inside an OPML. POST /api/popular/{id} still
  subscribes to anything on it.
- A Directory button sits beside Popular; both open the same list
  screen. The sidebar toolbar wraps rather than squeezing four buttons.
- Tests: the directory is A to Z, Popular is its top ten, Paid Show is
  in neither, and subscribing works from the directory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HdTEWQNrzyFULijigkmMn
This commit is contained in:
2026-09-11 14:13:16 +00:00
parent c0f4b0bcb2
commit f3825cfc57
7 changed files with 62 additions and 22 deletions

View File

@@ -46,7 +46,7 @@ const ctx = {
? { schedule: 'every 60m', every_mins: 60, download_dir: '/tmp', max_total_gb: 0, max_age_days: 0 }
: String(url).includes('/api/users')
? [{ id: 1, name: 'admin', admin: true, password: true }, { id: 2, name: 'sam', admin: false, password: false }]
: String(url).includes('/api/popular')
: /\/api\/(popular|directory)/.test(String(url))
? [{ id: 'f', title: 'A Feed', image: null, subscribers: 2, subscribed: true },
{ id: 'g', title: null, image: null, subscribers: 1, subscribed: false }]
: []),
@@ -86,7 +86,8 @@ const drive = [
['prefsModal', () => ctx.prefsModal()],
['usersModal', () => ctx.usersModal()],
['opmlModal', () => ctx.opmlModal()],
['showPopular', () => ctx.showPopular()],
['listedModal (popular)', () => ctx.listedModal('Popular', 'Top ten.', '/api/popular')],
['listedModal (directory)', () => ctx.listedModal('Directory', 'A to Z.', '/api/directory')],
['logsModal', () => ctx.logsModal()],
// `const S` is not reachable from here: top-level const/let do not become properties
// of a vm context the way var and function declarations do.

View File

@@ -579,6 +579,21 @@ test('Popular lists what everyone here reads, but never a private feed', async (
expect(listed).not.toContain('.xml');
expect((await piper.request.post('/api/popular/paid-show')).status()).toBe(400);
// Popular is the top ten of the directory, and the directory is every listed feed, A to Z.
const dir = await (await piper.request.get('/api/directory')).json();
const top = await (await piper.request.get('/api/popular')).json();
const names = dir.map(p => (p.title || p.id).toLowerCase());
expect(names).toEqual([...names].sort());
expect(top.length).toBe(Math.min(10, dir.length));
expect(top.every(t => dir.some(d => d.id === t.id))).toBe(true);
expect(dir.map(p => p.id)).not.toContain('paid-show');
// Subscribe from the directory this time; the popular list shares the same rows.
await piper.keyboard.press('Escape');
await piper.locator('#directoryFeeds').click();
await expect(offered.filter({ hasText: 'Test Show' })).toBeVisible();
await expect(offered.filter({ hasText: /Paid Show|paid-show/ })).toHaveCount(0);
const row = async () =>
(await (await piper.request.get('/api/popular')).json()).find(p => p.id === 'test-show');
const before = await row();