Skip to content
This repository was archived by the owner on Jun 22, 2026. It is now read-only.

Commit 29fd43b

Browse files
committed
fix: keep header refresh as backend refetch
1 parent e05127a commit 29fd43b

6 files changed

Lines changed: 67 additions & 7 deletions

File tree

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
- **Explicit empty states** for no-result source filters and searches
5757
- **Connectivity indicator** that shows a no-wifi icon while offline and silently refreshes the current view when the browser reconnects
5858
- **Scheduled refresh** every 1 hour on wall-clock boundaries in UTC+7
59+
- **Manual refresh** from the header re-fetches the current feed view from backend stored items only; it does **not** re-fetch upstream sources
5960
- **Persisted visited-link dimming** for feed card titles across reload/reopen using local storage
6061
- **PWA-ready assets and offline caching** including manifest, service worker, touch icons, cached shell assets, and cached `/api/items` responses for previously visited views
6162
- **Reconnect list refresh** re-fetches the current view from backend stored items only; it does **not** refresh upstream sources
@@ -263,7 +264,10 @@ Behavior:
263264
- runs on the next **N-hour wall-clock boundary** based on `FEEDREADER_REFRESH_INTERVAL_HOURS` (default: **1 hour**)
264265
- does **not** perform an immediate refresh just because the container starts
265266

266-
On-demand refresh is available through the CLI and `POST /api/refresh`.
267+
On-demand refresh is available through:
268+
269+
- the header refresh button for re-fetching the current backend-stored feed view
270+
- the CLI and `POST /api/refresh` for triggering an immediate upstream source refresh
267271

268272
---
269273

@@ -330,7 +334,7 @@ Presentation-layer note:
330334

331335
### Loading and empty states
332336

333-
- first-load bootstrap queries, source filter changes, searches, and `View more` all show an explicit toast-based loading state
337+
- first-load bootstrap queries, source filter changes, searches, `View more`, and header refresh all show an explicit toast-based loading state
334338
- source-filter changes use the generic loading toast text `Loading feed…`
335339
- source-filter and search requests that return zero items replace the list with an empty-state message instead of leaving stale cards on screen
336340
- `View more` disables itself while an append request is in flight and hides itself when the current result set has no further page

docs/assets/feedreader-home.png

79 Bytes
Loading

web/static/app.js

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
const searchForm = document.querySelector("[data-search-form]");
4545
const searchInput = document.querySelector("[data-search-input]");
4646
const searchSourceInput = document.querySelector("[data-search-source]");
47+
const refreshButton = document.querySelector("[data-refresh-button]");
4748
const themeToggle = document.querySelector("[data-theme-toggle]");
4849
const toast = document.querySelector("[data-toast]");
4950
const pageSize = Number(cardsGrid?.dataset.pageSize || 12);
@@ -68,6 +69,7 @@
6869
let searchTimer = null;
6970
let ignoreNextEmptySearchInput = false;
7071
let requestSequence = 0;
72+
let refreshInFlight = false;
7173
let feedLoading = false;
7274
let feedLoadingMode = "";
7375
let activeToastKind = "";
@@ -378,7 +380,7 @@
378380
const showButton = hasNext && loadedCount > 0;
379381
const loadingMore = feedLoading && feedLoadingMode === "append";
380382
viewMoreButton.hidden = !showButton;
381-
viewMoreButton.disabled = !showButton || feedLoading;
383+
viewMoreButton.disabled = !showButton || refreshInFlight || feedLoading;
382384
viewMoreButton.textContent = loadingMore ? "Loading…" : "View more";
383385
};
384386

@@ -533,6 +535,20 @@
533535
}
534536
};
535537

538+
const setRefreshButtonLoading = (active) => {
539+
if (!refreshButton) return;
540+
refreshButton.disabled = active;
541+
refreshButton.classList.toggle("is-loading", active);
542+
refreshButton.setAttribute(
543+
"aria-label",
544+
active ? "Refreshing feed" : "Refresh feed",
545+
);
546+
refreshButton.setAttribute(
547+
"title",
548+
active ? "Refreshing feed" : "Refresh feed",
549+
);
550+
};
551+
536552
const fetchItems = async ({
537553
source,
538554
query,
@@ -704,7 +720,7 @@
704720

705721
async function refetchCurrentViewAfterReconnect() {
706722
syncConnectivityState();
707-
if (!browserOnline || reconnectRefetchInFlight) {
723+
if (!browserOnline || refreshInFlight || reconnectRefetchInFlight) {
708724
return false;
709725
}
710726
reconnectRefetchInFlight = true;
@@ -718,6 +734,29 @@
718734
}
719735
}
720736

737+
async function refreshFeedList() {
738+
syncConnectivityState();
739+
if (!browserOnline || refreshInFlight) {
740+
return false;
741+
}
742+
refreshInFlight = true;
743+
cancelPendingSearch();
744+
setRefreshButtonLoading(true);
745+
renderFeedBody();
746+
try {
747+
await refetchCurrentView({ loadingMessage: "Refreshing feed…" });
748+
showToast("Feed refreshed", "success");
749+
return true;
750+
} catch (error) {
751+
showToast("Refresh failed", "error");
752+
return false;
753+
} finally {
754+
refreshInFlight = false;
755+
setRefreshButtonLoading(false);
756+
renderFeedBody();
757+
}
758+
}
759+
721760
function openConfigDialog() {
722761
syncConfigOptions();
723762
syncDensityOptions();
@@ -934,6 +973,12 @@
934973
});
935974
}
936975

976+
if (refreshButton) {
977+
refreshButton.addEventListener("click", async () => {
978+
await refreshFeedList();
979+
});
980+
}
981+
937982
if (themeToggle) {
938983
themeToggle.addEventListener("click", () => {
939984
applyTheme(root.dataset.theme === "dark" ? "light" : "dark");
@@ -974,6 +1019,7 @@
9741019
renderFilters();
9751020
renderSearch();
9761021
renderFeedBody();
1022+
setRefreshButtonLoading(false);
9771023
renderConnectionIndicator();
9781024

9791025
if (shouldBootstrapRefetch) {

web/static/service-worker.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
const SHELL_CACHE = 'reader-shell-v33';
1+
const SHELL_CACHE = 'reader-shell-v34';
22
const ITEMS_CACHE = "reader-items-v22";
33
const CORE_ASSETS = [
44
"/",
55
'/static/style.css?v=38',
6-
"/static/app.js?v=29",
6+
"/static/app.js?v=30",
77
"/static/source-icons/hackernews.svg",
88
"/static/source-icons/github.svg",
99
"/static/source-icons/huggingface.svg",

web/static/style.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,10 @@ body.is-dialog-open {
922922
font-size: 0.9rem;
923923
}
924924

925+
.refresh-button.is-loading .theme-icon {
926+
animation: spin 0.8s linear infinite;
927+
}
928+
925929
.view-more[hidden] {
926930
display: none;
927931
}

web/templates/index.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<link rel="shortcut icon" href="/favicon.svg?v=8" type="image/svg+xml" />
1818
<link rel="apple-touch-icon" href="/apple-touch-icon.png?v=8" />
1919
<link rel="stylesheet" href="/static/style.css?v=38" />
20-
<script src="/static/app.js?v=29" defer></script>
20+
<script src="/static/app.js?v=30" defer></script>
2121
</head>
2222
<body>
2323
<header class="shell page-header">
@@ -34,6 +34,12 @@ <h1 class="brand"><img class="brand-mark" src="/favicon.svg?v=7" alt="" aria-hid
3434
<path d="M4 4l16 16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.9"/>
3535
</svg>
3636
</span>
37+
<button class="icon-button refresh-button" type="button" data-refresh-button aria-label="Refresh feed" title="Refresh feed">
38+
<svg class="theme-icon" viewBox="0 0 24 24" aria-hidden="true">
39+
<path d="M20 12a8 8 0 1 1-2.34-5.66" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8"/>
40+
<path d="M20 4v5h-5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8"/>
41+
</svg>
42+
</button>
3743
<button class="icon-button config-toggle" type="button" data-source-config-open aria-label="Open reader settings" title="Open reader settings">
3844
<svg class="theme-icon" viewBox="0 0 24 24" aria-hidden="true">
3945
<path d="M4 7h8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.8"/>

0 commit comments

Comments
 (0)