mirror of
https://github.com/R0m1k3/FlowReader.git
synced 2026-10-12 01:38:16 +02:00
- New Reader (desktop + mobile): loads full content on open, prefetches the next article, reading progress and time, "Aa" settings (light / sepia / dark / auto theme, serif / sans / Atkinson Hyperlegible font, size, line height, measure), Space-to-next, keyboard shortcuts, focus management, inert background, URL-driven (?a=) so Back closes it. - Dashboard: cursor pagination, unread/all filter, CSS grid (no masonry), no re-sorting under the reader, optimistic cache patches instead of refetch storms, "new articles" banner, error/offline states, j/k/o/m/s/v shortcuts and help dialog. - Cards: keyboard-openable, lazy <img>, memoized, touch-only swipe. - Focus mode: framer drag (drop @use-gesture), Read button, undo restores unread state, arrow keys. - WebSocket: single connection, backoff, cache patching, no console logs. - Contrast fixes (on-nature tokens), 44px targets, lang="fr", reduced motion, self-hosted fonts, theme applied before first paint. - Code splitting + vendor chunk, prompt-based PWA updates, smaller precache, offline cache for opened articles and images, caches cleared on logout; upgrade react-router (CVE), drop unused deps and assets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
99 lines
3.2 KiB
TypeScript
99 lines
3.2 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import { create } from 'zustand';
|
|
import { patchArticle } from './useArticleActions';
|
|
|
|
interface LiveState {
|
|
/** Articles fetched since the list was last loaded (not yet shown). */
|
|
newCount: number;
|
|
addNew: (n: number) => void;
|
|
clearNew: () => void;
|
|
}
|
|
|
|
export const useLive = create<LiveState>((set) => ({
|
|
newCount: 0,
|
|
addNew: (n) => set((s) => ({ newCount: s.newCount + n })),
|
|
clearNew: () => set({ newCount: 0 }),
|
|
}));
|
|
|
|
interface WsEvent {
|
|
type: string;
|
|
payload?: Record<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* Keeps one WebSocket per session. Server events only concern the current
|
|
* user; read/favorite/summary updates patch the cache in place, new articles
|
|
* are announced (not auto-inserted) so the list never jumps while reading.
|
|
*/
|
|
export function useWebsocket() {
|
|
const qc = useQueryClient();
|
|
|
|
useEffect(() => {
|
|
let socket: WebSocket | null = null;
|
|
let retry = 0;
|
|
let retryTimer: number | undefined;
|
|
let feedsTimer: number | undefined;
|
|
let closed = false;
|
|
|
|
const refreshFeedsSoon = () => {
|
|
window.clearTimeout(feedsTimer);
|
|
feedsTimer = window.setTimeout(() => qc.invalidateQueries({ queryKey: ['feeds'] }), 1500);
|
|
};
|
|
|
|
const onMessage = (event: MessageEvent) => {
|
|
let msg: WsEvent;
|
|
try {
|
|
msg = JSON.parse(event.data);
|
|
} catch {
|
|
return;
|
|
}
|
|
const p = msg.payload ?? {};
|
|
switch (msg.type) {
|
|
case 'article_updated': {
|
|
const { id, ...rest } = p as { id: string } & Record<string, unknown>;
|
|
if (id) patchArticle(qc, id, rest);
|
|
if ('is_read' in rest) refreshFeedsSoon();
|
|
break;
|
|
}
|
|
case 'new_articles':
|
|
useLive.getState().addNew(Number(p.count) || 0);
|
|
refreshFeedsSoon();
|
|
break;
|
|
case 'articles_bulk_read':
|
|
qc.invalidateQueries({ queryKey: ['articles'] });
|
|
refreshFeedsSoon();
|
|
break;
|
|
case 'refresh_done':
|
|
refreshFeedsSoon();
|
|
break;
|
|
}
|
|
};
|
|
|
|
const connect = () => {
|
|
if (closed) return;
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
socket = new WebSocket(`${protocol}//${window.location.host}/api/v1/ws`);
|
|
socket.onopen = () => {
|
|
retry = 0;
|
|
};
|
|
socket.onmessage = onMessage;
|
|
socket.onclose = () => {
|
|
if (closed) return;
|
|
const delay = Math.min(30_000, 1000 * 2 ** retry++);
|
|
retryTimer = window.setTimeout(connect, delay);
|
|
};
|
|
socket.onerror = () => socket?.close();
|
|
};
|
|
|
|
connect();
|
|
|
|
return () => {
|
|
closed = true;
|
|
window.clearTimeout(retryTimer);
|
|
window.clearTimeout(feedsTimer);
|
|
socket?.close();
|
|
};
|
|
}, [qc]);
|
|
}
|