fix(thread-page): make thread auto updates opt-in (#1115)

* fix(thread-page): make thread auto updates opt-in

Wire `Auto` and `Update` to the same manual refresh path and cover the thread flow with an e2e harness.

* fix(thread-page): address PR review findings
This commit is contained in:
Tommaso Casaburi
2026-03-20 16:48:57 +08:00
committed by GitHub
parent ce2ad82c8c
commit 816281c607
13 changed files with 630 additions and 49 deletions
@@ -4,6 +4,7 @@ import { createRoot, type Root } from 'react-dom/client';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DesktopBoardButtons, MobileBoardButtons } from '../board-buttons';
import useThreadLiveUpdatesStore from '../../../stores/use-thread-live-updates-store';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
@@ -237,6 +238,7 @@ describe('BoardButtons', () => {
testState.sortType = 'active';
testState.subscribed = false;
testState.viewMode = 'compact';
useThreadLiveUpdatesStore.getState().resetState();
Object.defineProperty(globalThis, 'alert', {
configurable: true,
value: vi.fn(),
@@ -321,7 +323,7 @@ describe('BoardButtons', () => {
expect(testState.resetMock).toHaveBeenCalledTimes(1);
});
it('renders thread actions and post stats, then updates, auto-alerts, and scrolls to the bottom', async () => {
it('renders thread actions and post stats, then requests refreshes, toggles auto updates, and scrolls to the bottom', async () => {
testState.commentsByCid = {
'comment-1': {
cid: 'comment-1',
@@ -345,11 +347,36 @@ describe('BoardButtons', () => {
await clickButton('bottom');
await clickButton('update');
await clickButton('Auto');
const autoCheckbox = container.querySelector<HTMLInputElement>('input[type="checkbox"]');
expect(autoCheckbox?.checked).toBe(false);
await act(async () => {
autoCheckbox?.click();
});
expect(window.scrollTo).toHaveBeenCalledWith({ behavior: 'instant', top: 2400 });
expect(testState.resetMock).toHaveBeenCalledTimes(1);
expect(globalThis.alert).toHaveBeenCalledWith('posts_auto_update_info');
expect(useThreadLiveUpdatesStore.getState().updateRequestId).toBe(1);
expect(useThreadLiveUpdatesStore.getState().enabled).toBe(true);
expect(autoCheckbox?.checked).toBe(true);
});
it('keeps the update button enabled while a manual thread refresh is in progress', async () => {
testState.commentsByCid = {
'comment-1': {
cid: 'comment-1',
postCid: 'comment-1',
replyCount: 9,
},
};
useThreadLiveUpdatesStore.getState().startUpdate();
await renderWithRoute(createElement(DesktopBoardButtons), '/mu/thread/comment-1');
const updateButton = Array.from(container.querySelectorAll('button')).find((candidate) => candidate.textContent === 'update');
expect(updateButton?.hasAttribute('disabled')).toBe(false);
await clickButton('update');
expect(useThreadLiveUpdatesStore.getState().updateRequestId).toBe(1);
});
it('renders mobile mod-queue controls and clamps alert threshold updates', async () => {
+18 -24
View File
@@ -13,6 +13,7 @@ import useSortingStore from '../../stores/use-sorting-store';
import useAllFeedFilterStore from '../../stores/use-all-feed-filter-store';
import useModQueueStore from '../../stores/use-mod-queue-store';
import useFeedViewSettingsStore from '../../stores/use-feed-view-settings-store';
import useThreadLiveUpdatesStore from '../../stores/use-thread-live-updates-store';
import useCountLinksInReplies from '../../hooks/use-count-links-in-replies';
import useIsMobile from '../../hooks/use-is-mobile';
import CatalogFilters from '../catalog-filters';
@@ -35,7 +36,6 @@ interface BoardButtonsProps {
export const CatalogButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => {
const { t } = useTranslation();
const params = useParams();
const directories = useDirectories();
const createCatalogLink = () => {
@@ -158,9 +158,9 @@ export const RefreshButton = () => {
export const UpdateButton = () => {
const { t } = useTranslation();
const reset = useFeedResetStore((state) => state.reset);
const requestUpdate = useThreadLiveUpdatesStore((state) => state.requestUpdate);
return (
<button className='button' onClick={() => reset?.()}>
<button className='button' onClick={() => requestUpdate()}>
{t('update')}
</button>
);
@@ -169,26 +169,19 @@ export const UpdateButton = () => {
export const AutoButton = () => {
const { t } = useTranslation();
const isMobile = useIsMobile();
const handleAutoClick = () => {
window.alert(t('posts_auto_update_info'));
};
const autoUpdateEnabled = useThreadLiveUpdatesStore((state) => state.enabled);
const setAutoUpdateEnabled = useThreadLiveUpdatesStore((state) => state.setEnabled);
return (
<>
{isMobile ? (
<button className='button' onClick={handleAutoClick}>
<label>
<input type='checkbox' className={styles.autoCheckbox} checked disabled />
{t('Auto')}
</label>
</button>
) : (
<label onClick={handleAutoClick}>
{' '}
<input type='checkbox' className={styles.autoCheckbox} checked disabled /> {t('Auto')}
</label>
)}
</>
<label className={isMobile ? 'button' : undefined}>
<input
type='checkbox'
aria-label={t('Auto')}
className={styles.autoCheckbox}
checked={autoUpdateEnabled}
onChange={(event) => setAutoUpdateEnabled(event.target.checked)}
/>{' '}
{t('Auto')}
</label>
);
};
@@ -495,14 +488,15 @@ export const PostPageStats = () => {
const { t } = useTranslation();
const params = useParams();
const location = useLocation();
const autoUpdateEnabled = useThreadLiveUpdatesStore((state) => state.enabled);
const commentCid = params?.commentCid as string | undefined;
const resolvedAddress = useResolvedCommunityAddress();
const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any });
const communityAddress = resolvedAddress || accountComment?.communityAddress;
const comment = useComment({ commentCid });
const comment = useComment({ commentCid, autoUpdate: autoUpdateEnabled });
const postCid = comment?.postCid ?? commentCid;
const post = useComment({ commentCid: postCid });
const post = useComment({ commentCid: postCid, autoUpdate: autoUpdateEnabled });
const archived = isCommentArchived(post);
const { closed, pinned, replyCount } = post || {};