feat: add quiet reading with rare confirmations

Part of the quiet-user-feedback change; docs and specs land in their own repositories.
This commit is contained in:
2026-10-06 07:01:58 +03:00
parent c096fc70ef
commit 5b30bb802a
77 changed files with 1352 additions and 1259 deletions
@@ -5,7 +5,7 @@ import { useParams, useSearchParams } from "react-router";
import { type IncomeAnalytics, useIncomeAnalytics } from "@/entities/income";
import { useScreenSync } from "@/features/tbank-connection";
import { isValidPeriod, moscowDate } from "@/shared/lib";
import { isValidPeriod, moscowDate, useInitialAvailability } from "@/shared/lib";
import {
type IncomeDisplayModel,
@@ -27,19 +27,13 @@ type AccountIncomePageViewModel
display: IncomeDisplayModel;
form: ReturnType<typeof useForm<IncomeForm>>;
formRevision: string;
lastSuccessfulAt?: null | string;
onReset: () => void;
onSubmit: (values: IncomeForm) => void;
partial?: boolean;
refreshError: boolean;
refreshing: boolean;
retry: () => Promise<unknown> | undefined;
sourceError?: boolean;
state: "loaded";
}
| {
period: IncomePeriod;
retry: () => Promise<unknown> | undefined;
state: "error";
}
| { state: "loading" };
@@ -69,7 +63,7 @@ const useAccountIncomePageViewModel = (): AccountIncomePageViewModel => {
const [resetRevision, setResetRevision] = useState(0);
const analytics = useIncomeAnalytics(accountId, period);
const sync = useScreenSync(
useScreenSync(
{ accountId, from: period.from, screen: "income", to: period.to },
[analytics],
);
@@ -82,11 +76,20 @@ const useAccountIncomePageViewModel = (): AccountIncomePageViewModel => {
form.reset(draftPeriod(period));
}, [accountId, form, period]);
if (analytics.isLoading || !analytics.data) {
return analytics.isError || analytics.meta?.sync.state === "failed"
? { period, retry: sync.retry, state: "error" }
const availability = useInitialAvailability({
hasContent: !!analytics.data,
identity: `${accountId}:${period.from}:${period.to}`,
isError: analytics.isError || analytics.meta?.sync.state === "failed",
isFetching: analytics.isFetching,
});
if (availability === "loading" && analytics.isLoading)
return { state: "loading" };
if (!analytics.data)
return availability === "unavailable"
? { period, state: "error" }
: { state: "loading" };
}
const display = toIncomeDisplayModel(analytics.data, analytics.meta);
@@ -109,14 +112,9 @@ const useAccountIncomePageViewModel = (): AccountIncomePageViewModel => {
display,
form,
formRevision: `${accountId}:${period.from}:${period.to}:${resetRevision}`,
lastSuccessfulAt: analytics.meta?.lastSuccessfulAt,
onReset,
onSubmit,
partial: analytics.meta?.availability !== "complete",
refreshError: analytics.isError || sync.sourceError || sync.transportError,
refreshing: analytics.isFetching || sync.refreshing,
retry: sync.retry,
sourceError: sync.sourceError,
state: "loaded",
};
};