diff --git a/frontend/src/App.test.ts b/frontend/src/App.test.ts index 6ab72799..78d77628 100644 --- a/frontend/src/App.test.ts +++ b/frontend/src/App.test.ts @@ -8,6 +8,7 @@ import en from './i18n/locales/en'; import App from './App.vue'; import { setSettingsFromRawData } from './composables/core/useSettings'; import { getRecommendedFonts } from './utils/fontDetector'; +import { preserveSelectedArticle } from './stores/app'; // Create stub components for complex child components const createStub = (name: string) => ({ @@ -16,6 +17,15 @@ const createStub = (name: string) => ({ }); describe('App', () => { + it('preserves the selected article during a background refresh', () => { + const selected = { id: 75, title: 'Selected article' }; + const fresh = [{ id: 1, title: 'Fresh article' }]; + + expect(preserveSelectedArticle(fresh, [selected], 75)).toEqual([fresh[0], selected]); + expect(preserveSelectedArticle([selected], [selected], 75)).toEqual([selected]); + expect(preserveSelectedArticle(fresh, [selected], null)).toEqual(fresh); + }); + it('keeps long toast messages inside narrow viewports', () => { const toast = readFileSync('src/components/common/Toast.vue', 'utf8'); expect(toast).toContain('calc(100vw-2rem)'); diff --git a/frontend/src/stores/app.ts b/frontend/src/stores/app.ts index b4a1a5d3..fd7192eb 100644 --- a/frontend/src/stores/app.ts +++ b/frontend/src/stores/app.ts @@ -4,6 +4,18 @@ import type { Article, Feed, Tag, UnreadCounts, RefreshProgress } from '@/types/ import type { FilterCondition } from '@/types/filter'; import { useSettings } from '@/composables/core/useSettings'; +export function preserveSelectedArticle( + freshArticles: T[], + previousArticles: T[], + currentArticleId: number | null +): T[] { + if (!currentArticleId || freshArticles.some((article) => article.id === currentArticleId)) { + return freshArticles; + } + const selectedArticle = previousArticles.find((article) => article.id === currentArticleId); + return selectedArticle ? [...freshArticles, selectedArticle] : freshArticles; +} + export type Filter = 'all' | 'unread' | 'favorites' | 'readLater' | 'imageGallery' | ''; export type ThemePreference = 'light' | 'dark' | 'auto'; export type Theme = 'light' | 'dark'; @@ -44,7 +56,7 @@ export interface AppActions { setFeed: (feedId: number) => void; selectFeedInArticleList: (feedId: number, articleId?: number) => void; setCategory: (category: string) => void; - fetchArticles: (append?: boolean) => Promise; + fetchArticles: (append?: boolean, preserveExisting?: boolean) => Promise; loadMore: () => Promise; fetchFeeds: () => Promise; fetchUnreadCounts: () => Promise; @@ -192,13 +204,20 @@ export const useAppStore = defineStore('app', () => { } } - async function fetchArticles(append: boolean = false): Promise { + async function fetchArticles( + append: boolean = false, + preserveExisting: boolean = false + ): Promise { if (isLoading.value) return; + const previousArticles = articles.value; + // If not appending, reset to page 1 and clear articles if (!append) { page.value = 1; - articles.value = []; + if (!preserveExisting) { + articles.value = []; + } hasMore.value = true; } @@ -223,7 +242,9 @@ export const useAppStore = defineStore('app', () => { if (append) { articles.value = [...articles.value, ...data]; } else { - articles.value = data; + articles.value = preserveExisting + ? preserveSelectedArticle(data, previousArticles, currentArticleId.value) + : data; } } catch { // Error handled silently @@ -537,7 +558,7 @@ export const useAppStore = defineStore('app', () => { // Still refresh feeds and articles to get any updates from FreshRSS sync fetchFeeds(); - fetchArticles(); + fetchArticles(false, true); fetchUnreadCounts(); // Notify components that settings have been updated @@ -624,7 +645,7 @@ export const useAppStore = defineStore('app', () => { if (!data.is_running) { clearInterval(interval); fetchFeeds(); - fetchArticles(); + fetchArticles(false, true); fetchUnreadCounts(); // Notify components that settings have been updated (e.g., last_article_update) @@ -692,7 +713,7 @@ export const useAppStore = defineStore('app', () => { console.log('[FreshRSS] Sync completed detected, refreshing data...'); // Refresh all data await fetchFeeds(); - await fetchArticles(); + await fetchArticles(false, true); await fetchUnreadCounts(); }