Skip to content

Commit 26260f0

Browse files
authored
Merge pull request #8777 from daily-co/pre-79-when-eject_after_elapsed-eject_at_room_exp-or
PRE-79 Implement meeting end timer
2 parents a8d89da + c1cd150 commit 26260f0

File tree

4 files changed

+355
-1
lines changed

4 files changed

+355
-1
lines changed

src/DailyParticipants.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ export const localIdState = atom<string>({
4747
default: '',
4848
});
4949

50+
export const localJoinDateState = atom<Date | null>({
51+
key: RECOIL_PREFIX + 'local-joined-date',
52+
default: null,
53+
});
54+
5055
export const participantIdsState = atom<string[]>({
5156
key: RECOIL_PREFIX + 'participant-ids',
5257
default: [],
@@ -167,7 +172,17 @@ export const DailyParticipants: React.FC<React.PropsWithChildren<{}>> = ({
167172
}, [daily, initParticipants]);
168173
useDailyEvent('started-camera', handleInitEvent);
169174
useDailyEvent('access-state-updated', handleInitEvent);
170-
useDailyEvent('joining-meeting', handleInitEvent);
175+
useDailyEvent(
176+
'joining-meeting',
177+
useRecoilCallback(
178+
({ set }) =>
179+
() => {
180+
set(localJoinDateState, new Date());
181+
handleInitEvent();
182+
},
183+
[handleInitEvent]
184+
)
185+
);
171186
useDailyEvent(
172187
'joined-meeting',
173188
useCallback(

src/hooks/useRoomExp.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { useEffect, useState } from 'react';
2+
import { useRecoilValue } from 'recoil';
3+
4+
import { localJoinDateState } from '../DailyParticipants';
5+
import { useRoom } from './useRoom';
6+
7+
interface Countdown {
8+
hours: number;
9+
minutes: number;
10+
seconds: number;
11+
}
12+
13+
interface Props {
14+
onCountdown?(countdown: Countdown): void;
15+
}
16+
17+
export const useRoomExp = ({ onCountdown }: Props = {}) => {
18+
const localJoinDate = useRecoilValue(localJoinDateState);
19+
const room = useRoom();
20+
21+
const [ejectDate, setEjectDate] = useState<Date | null>(null);
22+
23+
useEffect(() => {
24+
const ejectAfterElapsed =
25+
room?.tokenConfig?.eject_after_elapsed ??
26+
room?.config?.eject_after_elapsed ??
27+
0;
28+
const expUTCTimeStamp = room?.tokenConfig?.exp ?? room?.config?.exp ?? 0;
29+
const ejectAtExp =
30+
room?.tokenConfig?.eject_at_token_exp ??
31+
room?.config?.eject_at_room_exp ??
32+
false;
33+
34+
let newEjectDate: Date = new Date(0);
35+
36+
if (ejectAfterElapsed && localJoinDate) {
37+
newEjectDate = new Date(
38+
localJoinDate.getTime() + 1000 * ejectAfterElapsed
39+
);
40+
}
41+
42+
if (ejectAtExp && expUTCTimeStamp) {
43+
const expDate = new Date(expUTCTimeStamp * 1000);
44+
if (
45+
!newEjectDate.getTime() ||
46+
(newEjectDate.getTime() > 0 && expDate < newEjectDate)
47+
)
48+
newEjectDate = expDate;
49+
}
50+
51+
if (newEjectDate.getTime() === 0) return;
52+
53+
setEjectDate((oldEjectDate) =>
54+
oldEjectDate?.getTime() !== newEjectDate.getTime()
55+
? newEjectDate
56+
: oldEjectDate
57+
);
58+
}, [
59+
localJoinDate,
60+
room?.config?.eject_after_elapsed,
61+
room?.config?.eject_at_room_exp,
62+
room?.config?.exp,
63+
room?.tokenConfig?.eject_after_elapsed,
64+
room?.tokenConfig?.eject_at_token_exp,
65+
room?.tokenConfig?.exp,
66+
]);
67+
68+
useEffect(() => {
69+
if (!ejectDate || ejectDate.getTime() === 0) return;
70+
71+
const interval = setInterval(() => {
72+
const eject = (ejectDate?.getTime() ?? 0) / 1000;
73+
const now = Date.now() / 1000;
74+
const diff = eject - now;
75+
if (diff < 0) return;
76+
const hours = Math.max(0, Math.floor(diff / 3600));
77+
const minutes = Math.max(0, Math.floor((diff % 3600) / 60));
78+
const seconds = Math.max(0, Math.floor(diff % 60));
79+
onCountdown?.({
80+
hours,
81+
minutes,
82+
seconds,
83+
});
84+
}, 1000);
85+
return () => {
86+
clearInterval(interval);
87+
};
88+
}, [ejectDate, onCountdown]);
89+
90+
return {
91+
ejectDate,
92+
};
93+
};

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export { usePermissions } from './hooks/usePermissions';
2828
export { useReceiveSettings } from './hooks/useReceiveSettings';
2929
export { useRecording } from './hooks/useRecording';
3030
export { useRoom } from './hooks/useRoom';
31+
export { useRoomExp } from './hooks/useRoomExp';
3132
export { useScreenAudioTrack } from './hooks/useScreenAudioTrack';
3233
export { ScreenShare, useScreenShare } from './hooks/useScreenShare';
3334
export { useScreenVideoTrack } from './hooks/useScreenVideoTrack';

test/hooks/useRoomExp.test.tsx

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
/// <reference types="@types/jest" />
2+
3+
import DailyIframe, {
4+
DailyCall,
5+
DailyEvent,
6+
DailyEventObjectNoPayload,
7+
} from '@daily-co/daily-js';
8+
import { act, renderHook } from '@testing-library/react-hooks';
9+
import faker from 'faker';
10+
import React from 'react';
11+
12+
import { DailyProvider } from '../../src/DailyProvider';
13+
import { useRoom } from '../../src/hooks/useRoom';
14+
import { useRoomExp } from '../../src/hooks/useRoomExp';
15+
16+
jest.mock('../../src/DailyDevices', () => ({
17+
...jest.requireActual('../../src/DailyDevices'),
18+
DailyDevices: (({ children }) => <>{children}</>) as React.FC,
19+
}));
20+
jest.mock('../../src/DailyLiveStreaming', () => ({
21+
...jest.requireActual('../../src/DailyLiveStreaming'),
22+
DailyLiveStreaming: (({ children }) => <>{children}</>) as React.FC,
23+
}));
24+
jest.mock('../../src/DailyRecordings', () => ({
25+
...jest.requireActual('../../src/DailyRecordings'),
26+
DailyRecordings: (({ children }) => <>{children}</>) as React.FC,
27+
}));
28+
jest.mock('../../src/DailyMeeting', () => ({
29+
...jest.requireActual('../../src/DailyMeeting'),
30+
DailyMeeting: (({ children }) => <>{children}</>) as React.FC,
31+
}));
32+
jest.mock('../../src/DailyRoom', () => ({
33+
...jest.requireActual('../../src/DailyRoom'),
34+
DailyRoom: (({ children }) => <>{children}</>) as React.FC,
35+
}));
36+
jest.mock('../../src/hooks/useRoom');
37+
38+
const createWrapper =
39+
(callObject: DailyCall = DailyIframe.createCallObject()): React.FC =>
40+
({ children }) =>
41+
<DailyProvider callObject={callObject}>{children}</DailyProvider>;
42+
43+
describe('useRoomExp', () => {
44+
beforeAll(() => {
45+
jest.useFakeTimers();
46+
});
47+
48+
beforeEach(() => {
49+
jest.clearAllMocks();
50+
jest.clearAllTimers();
51+
});
52+
53+
afterAll(() => {
54+
jest.useRealTimers();
55+
});
56+
57+
it('returns null initially', async () => {
58+
const daily = DailyIframe.createCallObject();
59+
const { result, waitFor } = renderHook(() => useRoomExp(), {
60+
wrapper: createWrapper(daily),
61+
});
62+
await waitFor(() => {
63+
expect(result.current.ejectDate).toBeNull();
64+
});
65+
});
66+
67+
describe('eject_after_elapsed', () => {
68+
it('should return correct ejectDate based on joining-meeting date', async () => {
69+
let localJoinDate: Date;
70+
(useRoom as jest.Mock).mockImplementation(() => ({
71+
id: faker.datatype.uuid(),
72+
name: faker.random.word(),
73+
domainConfig: {},
74+
tokenConfig: {},
75+
config: {
76+
eject_after_elapsed: 60,
77+
eject_at_room_exp: false,
78+
exp: 0,
79+
},
80+
}));
81+
const daily = DailyIframe.createCallObject();
82+
const { result, waitFor } = renderHook(() => useRoomExp(), {
83+
wrapper: createWrapper(daily),
84+
});
85+
86+
await waitFor(() => {
87+
expect(result.current.ejectDate).toBeNull();
88+
});
89+
90+
act(() => {
91+
const action: DailyEvent = 'joining-meeting';
92+
const payload: DailyEventObjectNoPayload = {
93+
action,
94+
};
95+
// @ts-ignore
96+
daily.emit(action, payload);
97+
localJoinDate = new Date();
98+
});
99+
100+
await waitFor(() => {
101+
expect(result.current.ejectDate).toBeInstanceOf(Date);
102+
expect(result.current.ejectDate?.getTime()).toBe(
103+
localJoinDate.getTime() + 60000
104+
);
105+
});
106+
});
107+
it('should call onCountdown correctly during the countdown', () => {
108+
(useRoom as jest.Mock).mockImplementation(() => ({
109+
id: faker.datatype.uuid(),
110+
name: faker.random.word(),
111+
domainConfig: {},
112+
tokenConfig: {},
113+
config: {
114+
eject_after_elapsed: 3610,
115+
eject_at_room_exp: false,
116+
exp: 0,
117+
},
118+
}));
119+
120+
const daily = DailyIframe.createCallObject();
121+
const onCountdown = jest.fn();
122+
renderHook(() => useRoomExp({ onCountdown }), {
123+
wrapper: createWrapper(daily),
124+
});
125+
126+
act(() => {
127+
const action: DailyEvent = 'joining-meeting';
128+
const payload: DailyEventObjectNoPayload = {
129+
action,
130+
};
131+
// @ts-ignore
132+
daily.emit(action, payload);
133+
});
134+
135+
expect(onCountdown).toHaveBeenCalledTimes(0);
136+
137+
act(() => {
138+
jest.advanceTimersByTime(1000);
139+
});
140+
141+
expect(onCountdown).toHaveBeenCalledTimes(1);
142+
expect(onCountdown).toHaveBeenCalledWith({
143+
hours: 1,
144+
minutes: 0,
145+
seconds: 9,
146+
});
147+
148+
act(() => {
149+
jest.advanceTimersByTime(3540000);
150+
});
151+
152+
expect(onCountdown).toHaveBeenCalledTimes(3541);
153+
expect(onCountdown).toHaveBeenLastCalledWith({
154+
hours: 0,
155+
minutes: 1,
156+
seconds: 9,
157+
});
158+
159+
act(() => {
160+
jest.advanceTimersByTime(30000);
161+
});
162+
163+
expect(onCountdown).toHaveBeenCalledTimes(3571);
164+
expect(onCountdown).toHaveBeenLastCalledWith({
165+
hours: 0,
166+
minutes: 0,
167+
seconds: 39,
168+
});
169+
});
170+
});
171+
172+
describe('eject_at_room_exp', () => {
173+
it('should return correct ejectDate', async () => {
174+
(useRoom as jest.Mock).mockImplementation(() => ({
175+
id: faker.datatype.uuid(),
176+
name: faker.random.word(),
177+
domainConfig: {},
178+
tokenConfig: {},
179+
config: {
180+
eject_after_elapsed: null,
181+
eject_at_room_exp: true,
182+
exp: 4100760732, // 2099-12-12T12:12:12.000Z
183+
},
184+
}));
185+
const daily = DailyIframe.createCallObject();
186+
const { result, waitFor } = renderHook(() => useRoomExp(), {
187+
wrapper: createWrapper(daily),
188+
});
189+
190+
await waitFor(() => {
191+
expect(result.current.ejectDate).toBeInstanceOf(Date);
192+
expect(result.current.ejectDate).toEqual(
193+
new Date('2099-12-12T12:12:12.000Z')
194+
);
195+
});
196+
});
197+
it('should call onCountdown correctly during the countdown', () => {
198+
const now = new Date();
199+
const nowUnix = Math.ceil(now.getTime() / 1000);
200+
const exp = nowUnix + 60;
201+
202+
(useRoom as jest.Mock).mockImplementation(() => ({
203+
id: faker.datatype.uuid(),
204+
name: faker.random.word(),
205+
domainConfig: {},
206+
tokenConfig: {},
207+
config: {
208+
eject_after_elapsed: null,
209+
eject_at_room_exp: true,
210+
exp,
211+
},
212+
}));
213+
214+
const daily = DailyIframe.createCallObject();
215+
const onCountdown = jest.fn();
216+
renderHook(() => useRoomExp({ onCountdown }), {
217+
wrapper: createWrapper(daily),
218+
});
219+
220+
expect(onCountdown).toHaveBeenCalledTimes(0);
221+
222+
act(() => {
223+
jest.advanceTimersByTime(1000);
224+
});
225+
226+
expect(onCountdown).toHaveBeenCalledTimes(1);
227+
expect(onCountdown).toHaveBeenCalledWith({
228+
hours: 0,
229+
minutes: 0,
230+
seconds: 59,
231+
});
232+
233+
act(() => {
234+
jest.advanceTimersByTime(2000);
235+
});
236+
237+
expect(onCountdown).toHaveBeenCalledTimes(3);
238+
expect(onCountdown).toHaveBeenLastCalledWith({
239+
hours: 0,
240+
minutes: 0,
241+
seconds: 57,
242+
});
243+
});
244+
});
245+
});

0 commit comments

Comments
 (0)