diff --git a/frontend/src/lib/components/feed/FeedListView.svelte b/frontend/src/lib/components/feed/FeedListView.svelte
index 4cf8fed..7036fc2 100644
--- a/frontend/src/lib/components/feed/FeedListView.svelte
+++ b/frontend/src/lib/components/feed/FeedListView.svelte
@@ -62,9 +62,9 @@
}
export function openSelectedReader() {
- const index = feedViewStore.selectedIndex;
- if (index < 0) return;
- const item = feedViewStore.currentItems[index];
+ const key = feedViewStore.selectedKey;
+ if (key === null) return;
+ const item = feedViewStore.currentItems.find((i) => i.key === key);
if (item) {
openReader(item);
}
@@ -86,7 +86,8 @@
}
async function handleExpand(index: number) {
- if (feedViewStore.expandedIndex === index) {
+ const key = feedViewStore.currentItems[index]?.key ?? null;
+ if (key !== null && feedViewStore.expandedKey === key) {
feedViewStore.collapse();
} else {
feedViewStore.select(index);
@@ -97,7 +98,8 @@
}
function handleSelect(index: number) {
- if (feedViewStore.selectedIndex === index) {
+ const key = feedViewStore.currentItems[index]?.key ?? null;
+ if (key !== null && feedViewStore.selectedKey === key) {
feedViewStore.deselect();
} else {
feedViewStore.select(index);
@@ -225,9 +227,9 @@
isSaved={itemLabelsStore.isSaved(article.guid)}
isShared={sharesStore.isShared(article.guid)}
shareNote={sharesStore.getShareNote(article.guid)}
- selected={preferences.expandAllItems || feedViewStore.selectedIndex === index}
- expanded={feedViewStore.expandedIndex === index}
- highlighted={feedViewStore.selectedIndex === index}
+ selected={preferences.expandAllItems || feedViewStore.selectedKey === displayItem.key}
+ expanded={feedViewStore.expandedKey === displayItem.key}
+ highlighted={feedViewStore.selectedKey === displayItem.key}
onToggleSave={() => onToggleSave(article)}
onToggleRead={() => handleToggleRead(article)}
onShare={() => sub && onShare(article, sub)}
@@ -250,9 +252,9 @@
{localArticle}
isRead={itemLabelsStore.isSocialRead(share.recordUri)}
isSaved={itemLabelsStore.isSaved(share.recordUri)}
- selected={preferences.expandAllItems || feedViewStore.selectedIndex === index}
- expanded={feedViewStore.expandedIndex === index}
- highlighted={feedViewStore.selectedIndex === index}
+ selected={preferences.expandAllItems || feedViewStore.selectedKey === displayItem.key}
+ expanded={feedViewStore.expandedKey === displayItem.key}
+ highlighted={feedViewStore.selectedKey === displayItem.key}
onToggleSave={() =>
itemLabelsStore.toggleSave(share.recordUri, 'share', share.itemUrl, share.itemTitle, {
type: 'share',
@@ -306,9 +308,9 @@
isShared={true}
shareNote={share.note}
reshareCount={share.reshareCount || 0}
- selected={preferences.expandAllItems || feedViewStore.selectedIndex === index}
- expanded={feedViewStore.expandedIndex === index}
- highlighted={feedViewStore.selectedIndex === index}
+ selected={preferences.expandAllItems || feedViewStore.selectedKey === displayItem.key}
+ expanded={feedViewStore.expandedKey === displayItem.key}
+ highlighted={feedViewStore.selectedKey === displayItem.key}
onToggleSave={() => onToggleSave(article)}
onToggleRead={() => handleToggleRead(article)}
onUnshare={() => onUnshare(share.articleGuid)}
@@ -328,9 +330,9 @@
document={doc}
isRead={itemLabelsStore.isSocialRead(doc.recordUri)}
isSaved={itemLabelsStore.isSaved(doc.recordUri)}
- selected={preferences.expandAllItems || feedViewStore.selectedIndex === index}
- expanded={feedViewStore.expandedIndex === index}
- highlighted={feedViewStore.selectedIndex === index}
+ selected={preferences.expandAllItems || feedViewStore.selectedKey === displayItem.key}
+ expanded={feedViewStore.expandedKey === displayItem.key}
+ highlighted={feedViewStore.selectedKey === displayItem.key}
onToggleSave={() =>
itemLabelsStore.toggleSave(
doc.recordUri,
diff --git a/frontend/src/lib/components/feed/SavedListView.svelte b/frontend/src/lib/components/feed/SavedListView.svelte
index dd02a50..b668678 100644
--- a/frontend/src/lib/components/feed/SavedListView.svelte
+++ b/frontend/src/lib/components/feed/SavedListView.svelte
@@ -274,9 +274,9 @@
}
export function openSelectedReader() {
- const index = feedViewStore.selectedIndex;
- if (index < 0) return;
- const item = feedViewStore.currentItems[index];
+ const key = feedViewStore.selectedKey;
+ if (key === null) return;
+ const item = feedViewStore.currentItems.find((i) => i.key === key);
if (item) {
openReader(item);
}
@@ -314,7 +314,7 @@
openReader(displayItem)}
onHover={() => handleSelect(index)}
onArchive={() => handleArchive(displayItem)}
diff --git a/frontend/src/lib/hooks/useFeedKeyboardShortcuts.svelte.ts b/frontend/src/lib/hooks/useFeedKeyboardShortcuts.svelte.ts
index 5f8ff7c..7daf5c2 100644
--- a/frontend/src/lib/hooks/useFeedKeyboardShortcuts.svelte.ts
+++ b/frontend/src/lib/hooks/useFeedKeyboardShortcuts.svelte.ts
@@ -37,16 +37,20 @@ export function useFeedKeyboardShortcuts(params: KeyboardShortcutsParams) {
return subscriptionsStore.subscriptions.find((s) => s.id === article.subscriptionId);
}
+ // Helper to resolve the currently-selected item by key. Returns null when
+ // nothing is selected or the previously-selected item is no longer present.
+ function getSelectedItem(): FeedDisplayItem | null {
+ const key = feedViewStore.selectedKey;
+ if (key === null) return null;
+ return feedViewStore.currentItems.find((i) => i.key === key) ?? null;
+ }
+
// Helper to get selected article info
function getSelectedArticle(): {
article: Article;
sub: Subscription;
} | null {
- const selectedIndex = feedViewStore.selectedIndex;
- if (selectedIndex < 0) return null;
-
- const items = feedViewStore.currentItems;
- const item = items[selectedIndex];
+ const item = getSelectedItem();
if (!item) return null;
const article = getArticleFromItem(item);
@@ -77,11 +81,7 @@ export function useFeedKeyboardShortcuts(params: KeyboardShortcutsParams) {
// Open selected item in new tab
function openSelectedItem() {
- const selectedIndex = feedViewStore.selectedIndex;
- if (selectedIndex < 0) return;
-
- const items = feedViewStore.currentItems;
- const item = items[selectedIndex];
+ const item = getSelectedItem();
if (!item) return;
let url: string;
@@ -103,11 +103,7 @@ export function useFeedKeyboardShortcuts(params: KeyboardShortcutsParams) {
// Toggle save on selected item (works for all item types)
function toggleSelectedSave() {
- const selectedIndex = feedViewStore.selectedIndex;
- if (selectedIndex < 0) return;
-
- const items = feedViewStore.currentItems;
- const item = items[selectedIndex];
+ const item = getSelectedItem();
if (!item) return;
if (item.type === 'article') {
@@ -198,11 +194,7 @@ export function useFeedKeyboardShortcuts(params: KeyboardShortcutsParams) {
// Toggle read/unread on selected item
function toggleSelectedRead() {
- const selectedIndex = feedViewStore.selectedIndex;
- if (selectedIndex < 0) return;
-
- const items = feedViewStore.currentItems;
- const item = items[selectedIndex];
+ const item = getSelectedItem();
if (!item) return;
if (item.type === 'article' || item.type === 'userShare') {
@@ -249,10 +241,12 @@ export function useFeedKeyboardShortcuts(params: KeyboardShortcutsParams) {
// Navigation actions
async function selectNextItem() {
const currentItems = feedViewStore.currentItems;
- const selectedIndex = feedViewStore.selectedIndex;
if (currentItems.length === 0) return;
- const nextIndex = Math.min(selectedIndex + 1, currentItems.length - 1);
+ const selectedKey = feedViewStore.selectedKey;
+ const currentIndex =
+ selectedKey === null ? -1 : currentItems.findIndex((i) => i.key === selectedKey);
+ const nextIndex = Math.min(currentIndex + 1, currentItems.length - 1);
feedViewStore.select(nextIndex);
// If we're at the last item, try to load more
@@ -266,10 +260,12 @@ export function useFeedKeyboardShortcuts(params: KeyboardShortcutsParams) {
async function selectPreviousItem() {
const currentItems = feedViewStore.currentItems;
- const selectedIndex = feedViewStore.selectedIndex;
if (currentItems.length === 0) return;
- feedViewStore.select(Math.max(selectedIndex - 1, 0));
+ const selectedKey = feedViewStore.selectedKey;
+ const currentIndex =
+ selectedKey === null ? -1 : currentItems.findIndex((i) => i.key === selectedKey);
+ feedViewStore.select(Math.max(currentIndex - 1, 0));
await tick();
params.scrollToCenter();
@@ -280,24 +276,24 @@ export function useFeedKeyboardShortcuts(params: KeyboardShortcutsParams) {
}
function hasSelected() {
- return auth.isAuthenticated && feedViewStore.selectedIndex >= 0;
+ return auth.isAuthenticated && feedViewStore.selectedKey !== null;
}
// Toggle expand action (or open bookmark reader in bookmarks view)
async function toggleExpand() {
- const selectedIndex = feedViewStore.selectedIndex;
- if (selectedIndex < 0) return;
+ const selectedKey = feedViewStore.selectedKey;
+ if (selectedKey === null) return;
if (feedViewStore.savedFilter && params.openSavedReader) {
params.openSavedReader();
return;
}
- const expandedIndex = feedViewStore.expandedIndex;
- if (expandedIndex === selectedIndex) {
+ if (feedViewStore.expandedKey === selectedKey) {
feedViewStore.collapse();
} else {
- feedViewStore.expand(selectedIndex);
+ const idx = feedViewStore.currentItems.findIndex((i) => i.key === selectedKey);
+ if (idx >= 0) feedViewStore.expand(idx);
}
await tick();
params.scrollToCenter();
@@ -368,9 +364,7 @@ export function useFeedKeyboardShortcuts(params: KeyboardShortcutsParams) {
description: 'Tag item',
category: 'Article',
action: () => {
- const idx = feedViewStore.selectedIndex;
- if (idx < 0) return;
- const item = feedViewStore.currentItems[idx];
+ const item = getSelectedItem();
if (!item) return;
if (feedViewStore.tagMenuItemKey === item.key) {
feedViewStore.closeTagMenu();
@@ -415,9 +409,7 @@ export function useFeedKeyboardShortcuts(params: KeyboardShortcutsParams) {
description: 'Archive/unarchive saved item',
category: 'Article',
action: () => {
- const idx = feedViewStore.selectedIndex;
- if (idx < 0) return;
- const item = feedViewStore.currentItems[idx];
+ const item = getSelectedItem();
if (!item) return;
const itemType = item.type === 'userShare' ? 'userShare' : item.type;
itemLabelsStore.toggleArchive(
@@ -434,7 +426,7 @@ export function useFeedKeyboardShortcuts(params: KeyboardShortcutsParams) {
description: 'Toggle highlight on paragraph',
category: 'Article',
action: () => params.toggleHighlight?.(),
- condition: () => hasSelected() && feedViewStore.expandedIndex >= 0,
+ condition: () => hasSelected() && feedViewStore.expandedKey !== null,
});
// Full-screen reader for feed items (not in bookmarks view, which uses Enter)
diff --git a/frontend/src/lib/stores/feedView.svelte.ts b/frontend/src/lib/stores/feedView.svelte.ts
index ca1f7cd..637cd54 100644
--- a/frontend/src/lib/stores/feedView.svelte.ts
+++ b/frontend/src/lib/stores/feedView.svelte.ts
@@ -192,8 +192,11 @@ export function matchesReadingLength(wc: number | null, bucket: ReadingLengthFil
function createFeedViewStore() {
// UI state
let showOnlyUnread = $state(true);
- let selectedIndex = $state(-1);
- let expandedIndex = $state(-1);
+ // Track selection/expansion by item key (stable across refreshes) rather than
+ // by array index — list refreshes re-sort items so an index would point at a
+ // different item after a refresh.
+ let selectedKey = $state(null);
+ let expandedKey = $state(null);
let loadedArticleCount = $state(DEFAULT_PAGE_SIZE);
// Tag menu state (which item key should show the tag menu, null = closed)
@@ -1056,16 +1059,20 @@ function createFeedViewStore() {
}
}
- function select(index: number) {
- if (index === selectedIndex) return;
+ function selectByKey(key: string | null) {
+ if (key === selectedKey) return;
- const items = currentItems;
- const item = items[index];
+ if (key === null) {
+ selectedKey = null;
+ expandedKey = null;
+ return;
+ }
+
+ const item = currentItems.find((i) => i.key === key);
if (!item) return;
- // Set selectedIndex first
- selectedIndex = index;
- expandedIndex = -1;
+ selectedKey = key;
+ expandedKey = null;
// Track the item to keep it visible in unread filter for this session
if (item.type === 'article') {
@@ -1112,23 +1119,27 @@ function createFeedViewStore() {
// userShare items don't auto-mark as read
}
+ function select(index: number) {
+ selectByKey(currentItems[index]?.key ?? null);
+ }
+
function deselect() {
- selectedIndex = -1;
- expandedIndex = -1;
+ selectedKey = null;
+ expandedKey = null;
// Don't clear session sets - items should stay visible until view changes
}
function expand(index: number) {
- expandedIndex = index;
+ expandedKey = currentItems[index]?.key ?? null;
}
function collapse() {
- expandedIndex = -1;
+ expandedKey = null;
}
function resetSelection() {
- selectedIndex = -1;
- expandedIndex = -1;
+ selectedKey = null;
+ expandedKey = null;
// Clear session sets when switching views/feeds
readArticleGuidsThisSession = new Set();
readShareUrisThisSession = new Set();
@@ -1248,11 +1259,17 @@ function createFeedViewStore() {
get currentItems() {
return currentItems;
},
+ get selectedKey() {
+ return selectedKey;
+ },
+ get expandedKey() {
+ return expandedKey;
+ },
get selectedIndex() {
- return selectedIndex;
+ return selectedKey === null ? -1 : currentItems.findIndex((i) => i.key === selectedKey);
},
get expandedIndex() {
- return expandedIndex;
+ return expandedKey === null ? -1 : currentItems.findIndex((i) => i.key === expandedKey);
},
get showOnlyUnread() {
return showOnlyUnread;
@@ -1365,6 +1382,7 @@ function createFeedViewStore() {
loadArticles,
loadMore,
select,
+ selectByKey,
deselect,
expand,
collapse,