aboutsummaryrefslogtreecommitdiffstats
path: root/frontend-vanilla
diff options
context:
space:
mode:
Diffstat (limited to 'frontend-vanilla')
-rw-r--r--frontend-vanilla/src/main.test.ts39
-rw-r--r--frontend-vanilla/src/main.ts26
2 files changed, 64 insertions, 1 deletions
diff --git a/frontend-vanilla/src/main.test.ts b/frontend-vanilla/src/main.test.ts
index 7cae34b..a8b6969 100644
--- a/frontend-vanilla/src/main.test.ts
+++ b/frontend-vanilla/src/main.test.ts
@@ -22,7 +22,11 @@ vi.mock('./api', () => ({
}));
// Mock IntersectionObserver as a constructor
+let observerCallback: IntersectionObserverCallback;
class MockIntersectionObserver {
+ constructor(callback: IntersectionObserverCallback) {
+ observerCallback = callback;
+ }
observe = vi.fn();
unobserve = vi.fn();
disconnect = vi.fn();
@@ -254,4 +258,39 @@ describe('main application logic', () => {
toggleBtn.click();
expect(store.sidebarVisible).toBe(!initialVisible);
});
+
+ it('should mark item as read when scrolled into view', () => {
+ const mockItem = {
+ _id: 123,
+ title: 'Scroll Test Item',
+ read: false,
+ url: 'http://example.com',
+ publish_date: '2023-01-01'
+ } as any;
+
+ store.setItems([mockItem]);
+ renderLayout();
+ renderItems();
+
+ const itemEl = document.querySelector(`.feed-item[data-id="123"]`);
+ expect(itemEl).not.toBeNull();
+
+ vi.mocked(apiFetch).mockResolvedValue({ ok: true } as Response);
+
+ // Simulate intersection
+ const entry = {
+ target: itemEl,
+ isIntersecting: true
+ } as IntersectionObserverEntry;
+
+ // This relies on the LAST created observer's callback being captured.
+ expect(observerCallback).toBeDefined();
+ // @ts-ignore
+ observerCallback([entry], {} as IntersectionObserver);
+
+ expect(apiFetch).toHaveBeenCalledWith(expect.stringContaining('/api/item/123'), expect.objectContaining({
+ method: 'PUT',
+ body: expect.stringContaining('"read":true')
+ }));
+ });
});
diff --git a/frontend-vanilla/src/main.ts b/frontend-vanilla/src/main.ts
index 93bee63..b167a18 100644
--- a/frontend-vanilla/src/main.ts
+++ b/frontend-vanilla/src/main.ts
@@ -18,6 +18,7 @@ let activeItemId: number | null = null;
// Cache elements (initialized in renderLayout)
let appEl: HTMLDivElement | null = null;
+let itemObserver: IntersectionObserver | null = null;
// Initial Layout (v2-style 2-pane)
export function renderLayout() {
@@ -216,6 +217,11 @@ export function renderFilters() {
export function renderItems() {
const { items, loading } = store;
+
+ if (itemObserver) {
+ itemObserver.disconnect();
+ itemObserver = null;
+ }
const contentArea = document.getElementById('content-area');
if (!contentArea || router.getCurrentRoute().path === '/settings') return;
@@ -246,6 +252,25 @@ export function renderItems() {
}, { threshold: 0.1 });
observer.observe(sentinel);
}
+
+ // Setup item observer for marking read
+ itemObserver = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ const target = entry.target as HTMLElement;
+ const id = parseInt(target.getAttribute('data-id') || '0');
+ if (id) {
+ const item = store.items.find(i => i._id === id);
+ if (item && !item.read) {
+ updateItem(id, { read: true });
+ itemObserver?.unobserve(target);
+ }
+ }
+ }
+ });
+ }, { threshold: 0.5 });
+
+ contentArea.querySelectorAll('.feed-item').forEach(el => itemObserver!.observe(el));
}
export function renderSettings() {
@@ -623,7 +648,6 @@ window.app = {
};
// Start
-// Start
export async function init() {
const authRes = await apiFetch('/api/auth');
if (!authRes || authRes.status === 401) {