Skip to content

Commit 9523b45

Browse files
authored
fix: schedule cooldown removal from useCooldownTimer hook instead of CooldownTimer (#2208)
1 parent ff554ad commit 9523b45

File tree

5 files changed

+130
-16
lines changed

5 files changed

+130
-16
lines changed

src/components/MessageInput/CooldownTimer.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,24 @@ export type CooldownTimerProps = {
44
cooldownInterval: number;
55
setCooldownRemaining: React.Dispatch<React.SetStateAction<number | undefined>>;
66
};
7-
export const CooldownTimer = ({ cooldownInterval, setCooldownRemaining }: CooldownTimerProps) => {
8-
const [seconds, setSeconds] = useState(cooldownInterval);
7+
export const CooldownTimer = ({ cooldownInterval }: CooldownTimerProps) => {
8+
const [seconds, setSeconds] = useState<number | undefined>();
99

1010
useEffect(() => {
11-
const countdownInterval = setInterval(() => {
12-
if (seconds > 0) {
11+
let countdownTimeout: ReturnType<typeof setTimeout>;
12+
if (typeof seconds === 'number' && seconds > 0) {
13+
countdownTimeout = setTimeout(() => {
1314
setSeconds(seconds - 1);
14-
} else {
15-
setCooldownRemaining(0);
16-
}
17-
}, 1000);
15+
}, 1000);
16+
}
17+
return () => {
18+
clearTimeout(countdownTimeout);
19+
};
20+
}, [seconds]);
1821

19-
return () => clearInterval(countdownInterval);
20-
});
22+
useEffect(() => {
23+
setSeconds(cooldownInterval ?? 0);
24+
}, [cooldownInterval]);
2125

2226
return (
2327
<div className='str-chat__message-input-cooldown' data-testid='cooldown-timer'>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import React from 'react';
2+
import { act, render, screen } from '@testing-library/react';
3+
import { CooldownTimer } from '../CooldownTimer';
4+
import '@testing-library/jest-dom';
5+
6+
jest.useFakeTimers();
7+
8+
const TIMER_TEST_ID = 'cooldown-timer';
9+
const remainingProp = 'cooldownInterval';
10+
describe('CooldownTimer', () => {
11+
it('renders CooldownTimer component', () => {
12+
render(<CooldownTimer />);
13+
expect(screen.getByTestId(TIMER_TEST_ID)).toHaveTextContent('0');
14+
});
15+
16+
it('initializes with correct state based on cooldownRemaining prop', () => {
17+
const props = { [remainingProp]: 10 };
18+
render(<CooldownTimer {...props} />);
19+
expect(screen.getByTestId(TIMER_TEST_ID)).toHaveTextContent('10');
20+
});
21+
22+
it('updates countdown logic correctly', () => {
23+
const cooldownRemaining = 5;
24+
const props = { [remainingProp]: cooldownRemaining };
25+
render(<CooldownTimer {...props} />);
26+
27+
for (let countDown = cooldownRemaining; countDown >= 0; countDown--) {
28+
expect(screen.getByTestId(TIMER_TEST_ID)).toHaveTextContent(countDown.toString());
29+
act(() => {
30+
jest.runAllTimers();
31+
});
32+
}
33+
expect(screen.getByTestId(TIMER_TEST_ID)).toHaveTextContent('0');
34+
});
35+
36+
it('resets countdown when cooldownRemaining prop changes', () => {
37+
const cooldownRemaining1 = 5;
38+
const cooldownRemaining2 = 10;
39+
const props1 = { [remainingProp]: cooldownRemaining1 };
40+
const props2 = { [remainingProp]: cooldownRemaining2 };
41+
const timeElapsedBeforeUpdate = 2;
42+
43+
const { rerender } = render(<CooldownTimer {...props1} />);
44+
45+
for (let round = timeElapsedBeforeUpdate; round > 0; round--) {
46+
act(() => {
47+
jest.runAllTimers();
48+
});
49+
}
50+
51+
expect(screen.getByTestId(TIMER_TEST_ID)).toHaveTextContent(
52+
(cooldownRemaining1 - timeElapsedBeforeUpdate).toString(),
53+
);
54+
55+
rerender(<CooldownTimer {...props2} />);
56+
57+
expect(screen.queryByTestId(TIMER_TEST_ID)).toHaveTextContent(cooldownRemaining2.toString());
58+
act(() => {
59+
jest.runAllTimers();
60+
});
61+
expect(screen.queryByTestId(TIMER_TEST_ID)).toHaveTextContent(
62+
(cooldownRemaining2 - 1).toString(),
63+
);
64+
});
65+
});

src/components/MessageInput/__tests__/MessageInput.test.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const mockedChannelData = generateChannel({
5858
thread: [threadMessage],
5959
});
6060

61+
const cooldown = 30;
6162
const filename = 'some.txt';
6263
const fileUploadUrl = 'http://www.getstream.io'; // real url, because ImagePreview will try to load the image
6364

@@ -1147,7 +1148,7 @@ function axeNoViolations(container) {
11471148

11481149
const renderWithActiveCooldown = async ({ messageInputProps = {} } = {}) => {
11491150
channel = chatClient.channel('messaging', mockedChannelData.channel.id);
1150-
channel.data.cooldown = 30;
1151+
channel.data.cooldown = cooldown;
11511152
channel.initialized = true;
11521153
const lastSentSecondsAhead = 5;
11531154
await render({
@@ -1263,6 +1264,17 @@ function axeNoViolations(container) {
12631264
expect(screen.queryByTestId(COOLDOWN_TIMER_TEST_ID)).not.toBeInTheDocument();
12641265
}
12651266
});
1267+
1268+
it('should be removed after cool-down period elapsed', async () => {
1269+
jest.useFakeTimers();
1270+
await renderWithActiveCooldown();
1271+
expect(screen.getByTestId(COOLDOWN_TIMER_TEST_ID)).toHaveTextContent(cooldown.toString());
1272+
act(() => {
1273+
jest.advanceTimersByTime(cooldown * 1000);
1274+
});
1275+
expect(screen.queryByTestId(COOLDOWN_TIMER_TEST_ID)).not.toBeInTheDocument();
1276+
jest.useRealTimers();
1277+
});
12661278
});
12671279
});
12681280
});

src/components/MessageInput/hooks/__tests__/useCooldownTimer.test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { useCooldownTimer } from '../useCooldownTimer';
55

66
import { ChannelStateProvider, ChatProvider } from '../../../../context';
77
import { getTestClient } from '../../../../mock-builders';
8+
import { act } from '@testing-library/react';
9+
10+
jest.useFakeTimers();
811

912
async function renderUseCooldownTimerHook({ channel, chatContext }) {
1013
const client = await getTestClient();
@@ -126,4 +129,23 @@ describe('useCooldownTimer', () => {
126129
const { result } = await renderUseCooldownTimerHook({ channel, chatContext });
127130
expect(result.current.cooldownRemaining).toBe(cooldown);
128131
});
132+
133+
it('remove the cooldown after the cooldown period elapses', async () => {
134+
const channel = { cid, data: { cooldown } };
135+
const chatContext = {
136+
latestMessageDatesByChannels: {
137+
[cid]: new Date(),
138+
},
139+
};
140+
141+
const { result } = await renderUseCooldownTimerHook({ channel, chatContext });
142+
143+
expect(result.current.cooldownRemaining).toBe(cooldown);
144+
145+
await act(() => {
146+
jest.advanceTimersByTime(cooldown * 1000);
147+
});
148+
149+
expect(result.current.cooldownRemaining).toBe(0);
150+
});
129151
});

src/components/MessageInput/hooks/useCooldownTimer.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,24 @@ export const useCooldownTimer = <
4040
Math.max(0, (new Date().getTime() - ownLatestMessageDate.getTime()) / 1000)
4141
: undefined;
4242

43-
setCooldownRemaining(
43+
const remaining =
4444
!skipCooldown &&
45-
typeof timeSinceOwnLastMessage !== 'undefined' &&
46-
cooldownInterval > timeSinceOwnLastMessage
45+
typeof timeSinceOwnLastMessage !== 'undefined' &&
46+
cooldownInterval > timeSinceOwnLastMessage
4747
? Math.round(cooldownInterval - timeSinceOwnLastMessage)
48-
: 0,
49-
);
48+
: 0;
49+
50+
setCooldownRemaining(remaining);
51+
52+
if (!remaining) return;
53+
54+
const timeout = setTimeout(() => {
55+
setCooldownRemaining(0);
56+
}, remaining * 1000);
57+
58+
return () => {
59+
clearTimeout(timeout);
60+
};
5061
}, [cooldownInterval, ownLatestMessageDate, skipCooldown]);
5162

5263
return {

0 commit comments

Comments
 (0)