On the Unread tab, a swipe back goes to the item just read (#50)
selectEntry took each read item out of the list the moment you moved on from it, so the item was not there for the back swipe (or k) to reach: it went to the one before, or to the list if the item had been first. Items read while turning from one to the next (a swipe, j and k) now stay in the list until the reader closes or another item is picked from the list, and a background refresh keeps them as it keeps the open one. Picking a row still drops the item left behind at once, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- On the Unread tab, a swipe back (or k) goes to the item you just read, instead of past it or
|
||||||
|
back to the list. The items read on the way leave the Unread tab once you close the reader.
|
||||||
|
|
||||||
## [0.9.0] - 2026-09-28
|
## [0.9.0] - 2026-09-28
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1255,6 +1255,34 @@ test.describe('touch gestures on a phone', () => {
|
|||||||
await expect(page.locator('body')).not.toHaveClass(/reading/);
|
await expect(page.locator('body')).not.toHaveClass(/reading/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('on the Unread tab, a swipe back goes to the item just read', async ({ page }) => {
|
||||||
|
await page.locator('#burger').click();
|
||||||
|
await page.locator('.feed', { hasText: 'Test Show' }).click();
|
||||||
|
await page.locator('.tabs button', { hasText: 'All' }).first().click();
|
||||||
|
await expect(page.locator('.ep').nth(1)).toBeVisible({ timeout: 20_000 });
|
||||||
|
// Earlier tests read these; both have to be unread to be on the Unread tab.
|
||||||
|
await page.evaluate(() => Promise.all(S.entries.map(e => setRead(e, false))));
|
||||||
|
await page.locator('.tabs button', { hasText: 'Unread' }).first().click();
|
||||||
|
await expect(page.locator('.ep').nth(1)).toBeVisible({ timeout: 20_000 });
|
||||||
|
const titles = await page.locator('.ep .t').allTextContents();
|
||||||
|
await page.locator('.ep').first().click();
|
||||||
|
const shown = page.locator('#detail .dt');
|
||||||
|
await expect(shown).toHaveText(titles[0]);
|
||||||
|
|
||||||
|
await drag(page, { x: 300, y: 400 }, { x: 80, y: 410 }); // left: the next item
|
||||||
|
await expect(shown).toHaveText(titles[1]);
|
||||||
|
// The first is read now, and used to be gone from the list already, so this went back to
|
||||||
|
// the list instead.
|
||||||
|
await drag(page, { x: 80, y: 400 }, { x: 300, y: 410 });
|
||||||
|
await expect(shown).toHaveText(titles[0]);
|
||||||
|
await drag(page, { x: 300, y: 400 }, { x: 80, y: 410 });
|
||||||
|
await expect(shown).toHaveText(titles[1]);
|
||||||
|
// Out of the reader, the ones read on the way leave the Unread tab as before.
|
||||||
|
await page.locator('#dback').click();
|
||||||
|
await expect(page.locator('body')).not.toHaveClass(/reading/);
|
||||||
|
await expect(page.locator('.ep .t', { hasText: titles[0] })).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
test('pulling the list down from its top checks the feed for new items', async ({ page }) => {
|
test('pulling the list down from its top checks the feed for new items', async ({ page }) => {
|
||||||
await page.locator('#burger').click();
|
await page.locator('#burger').click();
|
||||||
await page.locator('.feed', { hasText: 'Test Show' }).click();
|
await page.locator('.feed', { hasText: 'Test Show' }).click();
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ async function loadEntries(append?: boolean){
|
|||||||
// A background scan finishing refreshes the list from the server, which -- on the Unread
|
// A background scan finishing refreshes the list from the server, which -- on the Unread
|
||||||
// tab -- would drop the item you have open the moment reading it took it off the filter.
|
// tab -- would drop the item you have open the moment reading it took it off the filter.
|
||||||
// Keep it until you pick a different one; the next refresh after that no longer protects it.
|
// Keep it until you pick a different one; the next refresh after that no longer protects it.
|
||||||
if(S.filter==='unread') entries=entries.filter(e=>!e.read||e.guid===S.sel);
|
// The items turned past on the way to it are kept too, so a swipe back still finds them.
|
||||||
if(!append && S.sel && !entries.some(e=>e.guid===S.sel)){
|
const kept=e=>e.guid===S.sel||turned.has(e.guid);
|
||||||
const open=S.entries.find(e=>e.guid===S.sel);
|
if(S.filter==='unread') entries=entries.filter(e=>!e.read||kept(e));
|
||||||
if(open) entries=[open,...entries];
|
if(!append && S.sel){
|
||||||
|
const gone=S.entries.filter(e=>kept(e)&&!entries.some(n=>n.guid===e.guid));
|
||||||
|
entries=[...gone,...entries];
|
||||||
}
|
}
|
||||||
S.entries = entries;
|
S.entries = entries;
|
||||||
renderEntries();
|
renderEntries();
|
||||||
@@ -161,13 +163,24 @@ function markRead(e){
|
|||||||
setRead(e,true).catch(err=>{ e.read=false; readWrites.delete(readKey(e)); toast(err.message,true); });
|
setRead(e,true).catch(err=>{ e.read=false; readWrites.delete(readKey(e)); toast(err.message,true); });
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectEntry(e){
|
/// On the Unread tab, the items read while turning from one to the next (a swipe, j and k), kept
|
||||||
|
/// in the list until the reader closes or another item is picked from the list. Dropped as each
|
||||||
|
/// was left, a swipe back had nothing to go back to: the item just read was already gone.
|
||||||
|
const turned=new Set<string>();
|
||||||
|
function dropTurned(keep){
|
||||||
|
const gone=S.entries.filter(x=>turned.has(x.guid)&&x.read&&x.guid!==keep);
|
||||||
|
turned.clear();
|
||||||
|
if(!gone.length) return;
|
||||||
|
S.entries=S.entries.filter(x=>!gone.includes(x)); S.total-=gone.length;
|
||||||
|
for(const x of gone) $(`#eps .ep[data-guid="${CSS.escape(x.guid)}"]`)?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectEntry(e,turning=false){
|
||||||
// On the Unread tab the item you were reading goes as you move on, not whenever a refresh
|
// On the Unread tab the item you were reading goes as you move on, not whenever a refresh
|
||||||
// next happens to come along, which left a few read ones in the list for a while.
|
// next happens to come along, which left a few read ones in the list for a while.
|
||||||
const prev=S.filter==='unread' && S.sel!==e.guid && S.entries.find(x=>x.guid===S.sel);
|
if(S.filter==='unread' && S.sel && S.sel!==e.guid){
|
||||||
if(prev&&prev.read){
|
turned.add(S.sel);
|
||||||
S.entries=S.entries.filter(x=>x!==prev); S.total--;
|
if(!turning) dropTurned(e.guid);
|
||||||
$(`#eps .ep[data-guid="${CSS.escape(prev.guid)}"]`)?.remove();
|
|
||||||
}
|
}
|
||||||
S.sel=e.guid;
|
S.sel=e.guid;
|
||||||
markRead(e);
|
markRead(e);
|
||||||
@@ -224,6 +237,7 @@ function showDetail(e){
|
|||||||
document.body.classList.toggle('reading',!!e);
|
document.body.classList.toggle('reading',!!e);
|
||||||
syncTools(e);
|
syncTools(e);
|
||||||
if(!e){
|
if(!e){
|
||||||
|
dropTurned(S.sel);
|
||||||
box.innerHTML='<p class="empty">Pick an item to read it.</p>';
|
box.innerHTML='<p class="empty">Pick an item to read it.</p>';
|
||||||
if(files) files.innerHTML='<p class="empty">No files</p>';
|
if(files) files.innerHTML='<p class="empty">No files</p>';
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ function stepEntry(by){
|
|||||||
if(VIEWS[S.feed]?.url||!S.entries.length) return;
|
if(VIEWS[S.feed]?.url||!S.entries.length) return;
|
||||||
const i=S.entries.findIndex(x=>x.guid===S.sel);
|
const i=S.entries.findIndex(x=>x.guid===S.sel);
|
||||||
const e=S.entries[i<0?0:Math.min(S.entries.length-1,Math.max(0,i+by))];
|
const e=S.entries[i<0?0:Math.min(S.entries.length-1,Math.max(0,i+by))];
|
||||||
selectEntry(e);
|
selectEntry(e,true);
|
||||||
$(`#eps .ep[data-guid="${CSS.escape(e.guid)}"]`)?.scrollIntoView({block:'nearest'});
|
$(`#eps .ep[data-guid="${CSS.escape(e.guid)}"]`)?.scrollIntoView({block:'nearest'});
|
||||||
}
|
}
|
||||||
function stepFeed(by){
|
function stepFeed(by){
|
||||||
|
|||||||
Reference in New Issue
Block a user