aboutsummaryrefslogtreecommitdiffstats
path: root/frontend-vanilla/src
diff options
context:
space:
mode:
Diffstat (limited to 'frontend-vanilla/src')
-rw-r--r--frontend-vanilla/src/main.ts59
-rw-r--r--frontend-vanilla/src/polling.test.ts65
2 files changed, 123 insertions, 1 deletions
diff --git a/frontend-vanilla/src/main.ts b/frontend-vanilla/src/main.ts
index 8d88470..aa00bd3 100644
--- a/frontend-vanilla/src/main.ts
+++ b/frontend-vanilla/src/main.ts
@@ -303,6 +303,60 @@ export function renderItems() {
}
}
+// Polling fallback for infinite scroll (matches V1 behavior)
+// This ensures that even if scroll events are missed or layout shifts occur without scroll,
+// we still load more items when near the bottom.
+if (typeof window !== 'undefined') {
+ setInterval(() => {
+ // We need to check if we are scrolling the window or an element.
+ // In V3 layout, .main-content handles the scroll if it's overflow-y: auto.
+ // But if .main-content is behaving like the body, we might need to check window.innerHeight.
+
+ // Let's check the container first
+ const scrollRoot = document.getElementById('main-content');
+ // console.log('Polling...', { scrollRoot: !!scrollRoot, loading: store.loading, hasMore: store.hasMore });
+
+ if (store.loading || !store.hasMore) return;
+
+ if (scrollRoot) {
+ // DEBUG LOGGING
+ /*
+ console.log('Scroll Poll', {
+ scrollHeight: scrollRoot.scrollHeight,
+ scrollTop: scrollRoot.scrollTop,
+ clientHeight: scrollRoot.clientHeight,
+ offset: scrollRoot.scrollHeight - scrollRoot.scrollTop - scrollRoot.clientHeight,
+ docHeight: document.documentElement.scrollHeight,
+ winHeight: window.innerHeight,
+ winScroll: window.scrollY
+ });
+ */
+
+ // Check container scroll (if container is scrollable)
+ if (scrollRoot.scrollHeight > scrollRoot.clientHeight) {
+ if (scrollRoot.scrollHeight - scrollRoot.scrollTop - scrollRoot.clientHeight < 200) {
+ loadMore();
+ return;
+ }
+ }
+ }
+
+ // Fallback: Check window scroll (if main-content isn't the scroller)
+ // This matches V1 logic: $(document).height() - $(window).height() - $(window).scrollTop()
+ const docHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
+ const winHeight = window.innerHeight;
+ const winScroll = window.scrollY || document.documentElement.scrollTop;
+
+ // Only if document is actually scrollable
+ if (docHeight > winHeight) {
+ if (docHeight - winHeight - winScroll < 200) {
+ loadMore();
+ }
+ }
+
+ }, 1000);
+}
+
export function renderSettings() {
const contentArea = document.getElementById('content-area');
if (!contentArea) return;
@@ -613,7 +667,10 @@ export async function fetchItems(feedId?: string, tagName?: string, append: bool
const res = await apiFetch(`/api/stream?${params.toString()}`);
if (res.ok) {
const items = await res.json();
- store.setHasMore(items.length >= 50);
+ // V1 logic: keep loading as long as we get results.
+ // Backend limit is currently 15, so checking >= 50 caused premature stop.
+ // We accept one extra empty fetch at the end to be robust against page size changes.
+ store.setHasMore(items.length > 0);
store.setItems(items, append);
}
} finally {
diff --git a/frontend-vanilla/src/polling.test.ts b/frontend-vanilla/src/polling.test.ts
new file mode 100644
index 0000000..fa4b62f
--- /dev/null
+++ b/frontend-vanilla/src/polling.test.ts
@@ -0,0 +1,65 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { store } from './store';
+import { apiFetch } from './api';
+import './main'; // Import to start the polling interval
+
+// Mock api
+vi.mock('./api', () => ({
+ apiFetch: vi.fn()
+}));
+
+// Mock router to avoid errors during loadMore
+vi.mock('./router', () => ({
+ router: {
+ getCurrentRoute: () => ({ params: {}, query: new URLSearchParams() }),
+ updateQuery: vi.fn(),
+ navigate: vi.fn(),
+ addEventListener: vi.fn()
+ }
+}));
+
+describe('Infinite Scroll Polling', () => {
+ beforeEach(() => {
+ // Use real timers because the interval starts at module import time
+ vi.useRealTimers();
+ document.body.innerHTML = '<div id="main-content"></div>';
+ store.setItems(Array(50).fill({ _id: 1 }));
+ store.setHasMore(true);
+ store.setLoading(false);
+ vi.clearAllMocks();
+ });
+
+ it('should trigger loadMore via polling when near bottom', async () => {
+ const scrollRoot = document.getElementById('main-content')!;
+
+ // Mock scroll properties
+ Object.defineProperty(scrollRoot, 'scrollHeight', { value: 2000, configurable: true });
+ Object.defineProperty(scrollRoot, 'clientHeight', { value: 200, configurable: true });
+ // Use defineProperty for scrollTop to ensure it overrides native behavior in JSDOM
+ Object.defineProperty(scrollRoot, 'scrollTop', { value: 1750, configurable: true });
+
+ // Mock apiFetch response
+ vi.mocked(apiFetch).mockResolvedValue({
+ ok: true,
+ json: async () => []
+ } as Response);
+
+ // Wait for interval (1000ms) + buffer
+ await new Promise(resolve => setTimeout(resolve, 1100));
+
+ // Check if apiFetch was called
+ expect(apiFetch).toHaveBeenCalledWith(expect.stringContaining('/api/stream'));
+ });
+
+ it('should NOT trigger loadMore via polling when far from bottom', async () => {
+ const scrollRoot = document.getElementById('main-content')!;
+
+ Object.defineProperty(scrollRoot, 'scrollHeight', { value: 2000, configurable: true });
+ Object.defineProperty(scrollRoot, 'clientHeight', { value: 200, configurable: true });
+ Object.defineProperty(scrollRoot, 'scrollTop', { value: 100, configurable: true });
+
+ await new Promise(resolve => setTimeout(resolve, 1100));
+
+ expect(apiFetch).not.toHaveBeenCalled();
+ });
+});