fix(settings): show latest update feedback

This commit is contained in:
Tommaso Casaburi
2026-05-13 23:55:38 +07:00
parent 29f5e67c72
commit c8640a8010
38 changed files with 144 additions and 36 deletions
@@ -92,6 +92,7 @@ describe('InterfaceSettings', () => {
useFeedViewSettingsStore.getState().setEnableInfiniteScroll(false);
useAppUpdateStore.setState({
availableUpdate: null,
appUpdateCheckStatus: 'idle',
isApplyingUpdate: false,
isCheckingForUpdate: false,
applyAppUpdate: testState.applyAppUpdateMock,
@@ -106,6 +107,8 @@ describe('InterfaceSettings', () => {
afterEach(() => {
act(() => root.unmount());
useAppUpdateStore.getState().clearAppUpdateCheckStatus();
vi.useRealTimers();
container.remove();
setItemSpy.mockRestore();
vi.unstubAllGlobals();
@@ -230,6 +233,7 @@ describe('InterfaceSettings', () => {
});
it('checks for app updates when the check button is pressed', async () => {
testState.refreshAvailableUpdateMock.mockResolvedValueOnce(null);
render(createElement(InterfaceSettings));
await settleLazyImports();
@@ -245,6 +249,33 @@ describe('InterfaceSettings', () => {
expect(testState.applyAppUpdateMock).not.toHaveBeenCalled();
});
it('shows then clears the latest-version status when no app update is available', async () => {
vi.useFakeTimers();
testState.refreshAvailableUpdateMock.mockResolvedValueOnce(null);
render(createElement(InterfaceSettings));
await settleLazyImports();
const button = findButtonByText('Check');
expect(button).toBeTruthy();
await act(async () => {
button?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
expect(container.textContent).toContain('app_is_up_to_date');
act(() => {
vi.advanceTimersByTime(3999);
});
expect(container.textContent).toContain('app_is_up_to_date');
act(() => {
vi.advanceTimersByTime(1);
});
expect(container.textContent).not.toContain('app_is_up_to_date');
});
it('applies the available app update when the button is pressed', async () => {
useAppUpdateStore.setState({
availableUpdate: {
@@ -8,8 +8,10 @@ const UpdateButton = () => {
const availableUpdate = useAppUpdateStore((state) => state.availableUpdate);
const isApplyingUpdate = useAppUpdateStore((state) => state.isApplyingUpdate);
const isCheckingForUpdate = useAppUpdateStore((state) => state.isCheckingForUpdate);
const appUpdateCheckStatus = useAppUpdateStore((state) => state.appUpdateCheckStatus);
const applyAppUpdate = useAppUpdateStore((state) => state.applyAppUpdate);
const refreshAvailableUpdate = useAppUpdateStore((state) => state.refreshAvailableUpdate);
const showAppUpdateUpToDateStatus = useAppUpdateStore((state) => state.showAppUpdateUpToDateStatus);
const handleUpdateAction = async () => {
try {
@@ -18,13 +20,17 @@ const UpdateButton = () => {
return;
}
await refreshAvailableUpdate();
const update = await refreshAvailableUpdate();
if (!update) {
showAppUpdateUpToDateStatus();
}
} catch (error) {
alert(String(error));
}
};
const buttonLabel = availableUpdate ? t('download') : t('check');
const isBusy = isApplyingUpdate || isCheckingForUpdate;
const shouldShowUpToDateStatus = !isCheckingForUpdate && !availableUpdate && appUpdateCheckStatus === 'upToDate';
return (
<>
@@ -36,6 +42,11 @@ const UpdateButton = () => {
{t('checking_for_updates')}
</span>
)}
{shouldShowUpToDateStatus && (
<span className={styles.updateStatus} aria-live='polite'>
{t('app_is_up_to_date')}
</span>
)}
{!isCheckingForUpdate && availableUpdate && (
<span className={styles.updateStatus} aria-live='polite'>
{t('new_version_found')}:&nbsp;
+31
View File
@@ -1,19 +1,50 @@
import { create } from 'zustand';
import { applyAvailableAppUpdate, resolveAvailableAppUpdate, type AvailableAppUpdate } from '../lib/app-update';
type AppUpdateCheckStatus = 'idle' | 'upToDate';
interface AppUpdateState {
availableUpdate: AvailableAppUpdate | null;
appUpdateCheckStatus: AppUpdateCheckStatus;
isApplyingUpdate: boolean;
isCheckingForUpdate: boolean;
clearAppUpdateCheckStatus: () => void;
showAppUpdateUpToDateStatus: () => void;
refreshAvailableUpdate: () => Promise<AvailableAppUpdate | null>;
applyAppUpdate: () => Promise<void>;
}
const UP_TO_DATE_STATUS_TIMEOUT_MS = 4000;
let appUpdateCheckStatusTimeout: ReturnType<typeof setTimeout> | null = null;
const clearAppUpdateCheckStatusTimeout = () => {
if (appUpdateCheckStatusTimeout !== null) {
clearTimeout(appUpdateCheckStatusTimeout);
appUpdateCheckStatusTimeout = null;
}
};
const scheduleAppUpdateCheckStatusClear = (clearStatus: () => void) => {
clearAppUpdateCheckStatusTimeout();
appUpdateCheckStatusTimeout = setTimeout(clearStatus, UP_TO_DATE_STATUS_TIMEOUT_MS);
};
const useAppUpdateStore = create<AppUpdateState>((set, get) => ({
availableUpdate: null,
appUpdateCheckStatus: 'idle',
isApplyingUpdate: false,
isCheckingForUpdate: false,
clearAppUpdateCheckStatus: () => {
clearAppUpdateCheckStatusTimeout();
set({ appUpdateCheckStatus: 'idle' });
},
showAppUpdateUpToDateStatus: () => {
set({ appUpdateCheckStatus: 'upToDate' });
scheduleAppUpdateCheckStatusClear(get().clearAppUpdateCheckStatus);
},
refreshAvailableUpdate: async () => {
get().clearAppUpdateCheckStatus();
set({ isCheckingForUpdate: true });
try {
const availableUpdate = await resolveAvailableAppUpdate();