mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(pwa): fix pull-to-refresh's at-top check and extend coverage
The authenticated shell's actual scroll container is <main id="main-content"> (AuthShell sets it overflow-y-auto inside a height-locked layout); document.scrollingElement never moves there, so the gesture's "only arm at the top of the page" check was permanently true regardless of how far a user had scrolled a long list. Point it at the real scroll container instead, with the old document-root check kept as a fallback for the few body-scrolled surfaces outside the shell. Extend pull-to-refresh from 4 pages (labs/checkups/mood/measurements) to the dashboard, /insights, /medications, and the documents vault — the sharpest PWA-resume-staleness surfaces, per the audit: a background-refreshed dose or a cross-device write otherwise sits stale until a full remount or the staleTime window lapses.
This commit is contained in:
1 parent
c1c3aa9754
commit
b00450aec8
5 files changed
+90
-8
No files matched your search
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment, type ReactNode } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Fragment, useCallback, type ReactNode } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { SlidersHorizontal, TrendingUp } from "lucide-react";
|
||||
@@ -11,6 +11,8 @@ import { queryKeys } from "@/lib/query-keys";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { useFeatureFlags } from "@/hooks/use-feature-flags";
|
||||
import { useInsightsLayoutQuery } from "@/hooks/use-insights-layout";
|
||||
import { usePullToRefresh } from "@/hooks/use-pull-to-refresh";
|
||||
import { PullToRefreshIndicator } from "@/components/ui/pull-to-refresh-indicator";
|
||||
import {
|
||||
orderedVisibleSectionIds,
|
||||
type InsightsSectionId,
|
||||
@@ -263,6 +265,20 @@ export default function InsightsPage() {
|
||||
const { isAuthenticated, user } = useAuth();
|
||||
const mounted = useMounted();
|
||||
const { t } = useTranslations();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// v1.30.1 M12 — the overview fans out into ~6 independent queries
|
||||
// (comprehensive / advisor / analytics / snapshot / derived batch /
|
||||
// layout) mounted by different hooks below, so there's no single
|
||||
// `refetch()` that covers the page. Refetching every currently-mounted
|
||||
// ("active") query is the same blanket approach the audit's fallback
|
||||
// suggests for PWA-resume staleness, and it's the right shape here
|
||||
// regardless of resume vs. an explicit pull.
|
||||
const refreshInsights = useCallback(
|
||||
() => queryClient.refetchQueries({ type: "active" }),
|
||||
[queryClient],
|
||||
);
|
||||
const pull = usePullToRefresh({ onRefresh: refreshInsights });
|
||||
|
||||
// v1.4.33 IW9 — scroll-to-top on route mount centralised in the
|
||||
// shared `useScrollResetOnRoute()` hook. The mother page + the
|
||||
@@ -531,6 +547,7 @@ export default function InsightsPage() {
|
||||
// zero-gap). Each section owns its OWN `SectionHeading` + `space-y-3` to its
|
||||
// card, so the rhythm is even regardless of which sections are present.
|
||||
<div className="space-y-6">
|
||||
<PullToRefreshIndicator {...pull} />
|
||||
<HeroStrip
|
||||
briefing={briefingPayload}
|
||||
updatedAt={heroStripUpdatedAt}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import {
|
||||
invalidateKeys,
|
||||
medicationDependentKeys,
|
||||
queryKeys,
|
||||
} from "@/lib/query-keys";
|
||||
import { usePullToRefresh } from "@/hooks/use-pull-to-refresh";
|
||||
import { PullToRefreshIndicator } from "@/components/ui/pull-to-refresh-indicator";
|
||||
import { MedicationWizardDialog } from "@/components/medications/wizard/medication-wizard-dialog";
|
||||
import { MedicationCard } from "@/components/medications/medication-card";
|
||||
import { Glp1MedicationCard } from "@/components/medications/glp1-medication-card";
|
||||
@@ -221,6 +227,17 @@ export default function MedicationsPage() {
|
||||
refetchOnMount: "always",
|
||||
});
|
||||
|
||||
// v1.30.1 M12 — pull-to-refresh parity with labs/checkups/mood/measurements.
|
||||
// Medications is one of the sharpest PWA-resume-staleness surfaces (a
|
||||
// background-refreshed take/skip from a notification, or a cross-device
|
||||
// write, wouldn't otherwise show up until the 5-min staleTime lapses or a
|
||||
// full remount re-triggers `refetchOnMount: "always"` above).
|
||||
const refreshMedications = useCallback(
|
||||
() => invalidateKeys(queryClient, medicationDependentKeys),
|
||||
[queryClient],
|
||||
);
|
||||
const pull = usePullToRefresh({ onRefresh: refreshMedications });
|
||||
|
||||
// v1.29.x — once the highlighted card is in the DOM, scroll it into view
|
||||
// and clear the highlight state after a few seconds so the ring is a
|
||||
// one-time "you tapped here" cue, not a permanent tint (mirrors the
|
||||
@@ -363,6 +380,7 @@ export default function MedicationsPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PullToRefreshIndicator {...pull} />
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<h1
|
||||
|
||||
+20
-2
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import React, { Suspense, useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import React, { Suspense, useCallback, useMemo, useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useMounted } from "@/hooks/use-mounted";
|
||||
import { usePullToRefresh } from "@/hooks/use-pull-to-refresh";
|
||||
import { PullToRefreshIndicator } from "@/components/ui/pull-to-refresh-indicator";
|
||||
import {
|
||||
Activity,
|
||||
Bone,
|
||||
@@ -190,8 +192,23 @@ export default function DashboardPageClient() {
|
||||
const mounted = useMounted();
|
||||
const { t } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
const queryClient = useQueryClient();
|
||||
const [quickEntryDialog, setQuickEntryDialog] =
|
||||
useState<QuickEntryDialog>(null);
|
||||
|
||||
// v1.30.1 M12 — pull-to-refresh parity for the dashboard, named as one
|
||||
// of the sharpest PWA-resume-staleness surfaces in the audit (it
|
||||
// already opts back into `refetchOnWindowFocus`, but a PWA resumed
|
||||
// from the home screen/app-switcher, not a browser tab focus event,
|
||||
// is the more common mobile path and that never fires a focus event).
|
||||
// The dashboard fans out into a dozen-plus independent per-tile/chart
|
||||
// queries, so — like `/insights` — refetching every currently-active
|
||||
// query is the only tractable "refresh everything visible" shape.
|
||||
const refreshDashboard = useCallback(
|
||||
() => queryClient.refetchQueries({ type: "active" }),
|
||||
[queryClient],
|
||||
);
|
||||
const pull = usePullToRefresh({ onRefresh: refreshDashboard });
|
||||
// v1.4.29 M5 — the three inline dashboard queries default to a
|
||||
// 0-ms `staleTime`, so a tab-focus-and-return triggered a refetch
|
||||
// storm. None of these are real-time data; minute-scale staleness
|
||||
@@ -785,6 +802,7 @@ export default function DashboardPageClient() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PullToRefreshIndicator {...pull} />
|
||||
<DashboardHeader onQuickEntry={setQuickEntryDialog} />
|
||||
|
||||
{/* S2 — the Today hero, promoted above the tile strip (MARC sign-off
|
||||
|
||||
@@ -38,6 +38,8 @@ import { PageHeader } from "@/components/ui/page-header";
|
||||
import { QueryErrorCard } from "@/components/ui/query-error-card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { usePullToRefresh } from "@/hooks/use-pull-to-refresh";
|
||||
import { PullToRefreshIndicator } from "@/components/ui/pull-to-refresh-indicator";
|
||||
import { apiGet, apiPost } from "@/lib/api/api-fetch";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { invalidateKeys, queryKeys } from "@/lib/query-keys";
|
||||
@@ -233,6 +235,18 @@ export function DocumentsView() {
|
||||
[list.data],
|
||||
);
|
||||
|
||||
// v1.30.1 M12 — pull-to-refresh parity with labs/checkups/mood/
|
||||
// measurements/medications. The vault is one of the sharpest PWA-resume-
|
||||
// staleness surfaces named in the audit: a document indexed server-side
|
||||
// while the tab was backgrounded (auto-index / AI summary) otherwise
|
||||
// stays invisible until the bounded processing poll above happens to
|
||||
// catch it or the user navigates away and back.
|
||||
const refreshDocuments = useCallback(
|
||||
() => Promise.all([list.refetch(), usage.refetch()]),
|
||||
[list, usage],
|
||||
);
|
||||
const pull = usePullToRefresh({ onRefresh: refreshDocuments });
|
||||
|
||||
const upload = useDocumentUpload(
|
||||
usage.data ? { maxFileBytes: usage.data.maxFileBytes } : undefined,
|
||||
);
|
||||
@@ -595,6 +609,7 @@ export function DocumentsView() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PullToRefreshIndicator {...pull} />
|
||||
<PageHeader
|
||||
title={t("documents.title")}
|
||||
description={t("documents.subtitle")}
|
||||
|
||||
@@ -60,8 +60,22 @@ export function usePullToRefresh({
|
||||
let busy = false;
|
||||
let lastDistance = 0;
|
||||
|
||||
const atTop = () =>
|
||||
(document.scrollingElement?.scrollTop ?? window.scrollY) <= 0;
|
||||
// v1.30.1 M12 — the authenticated shell's actual scroll container is
|
||||
// `<main id="main-content">` (`AuthShell` sets it `overflow-y-auto`
|
||||
// inside a height-locked `h-dvh` layout); `document.scrollingElement`
|
||||
// never moves there; it stays permanently at 0. Checking the document
|
||||
// root meant "at top" was always true regardless of how far the user
|
||||
// had actually scrolled a long list — a downward drag mid-scroll could
|
||||
// arm the pull gesture instead of just scrolling the list. Falls back
|
||||
// to the document root for the few body-scrolled surfaces (public
|
||||
// pages) that render outside the shell's `#main-content`.
|
||||
const atTop = () => {
|
||||
const main = document.getElementById("main-content");
|
||||
const scrollTop = main
|
||||
? main.scrollTop
|
||||
: (document.scrollingElement?.scrollTop ?? window.scrollY);
|
||||
return scrollTop <= 0;
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
startY = null;
|
||||
|
||||
Reference in new issue
Block a user