2024-01-17 19:29:19 +08:00
|
|
|
import { useSubscription, useMutation } from '@apollo/client';
|
2023-09-25 21:04:28 +08:00
|
|
|
import React, { useEffect, useRef, useState } from 'react';
|
2024-04-30 23:45:05 +08:00
|
|
|
import GET_TIMER, { GetTimerResponse } from '../../../core/graphql/queries/timer';
|
2023-09-25 21:04:28 +08:00
|
|
|
import logger from '/imports/startup/client/logger';
|
|
|
|
import Styled from './styles';
|
|
|
|
import Icon from '/imports/ui/components/common/icon/icon-ts/component';
|
|
|
|
import humanizeSeconds from '/imports/utils/humanizeSeconds';
|
|
|
|
import useTimeSync from '/imports/ui/core/local-states/useTimeSync';
|
|
|
|
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
2024-04-30 23:45:05 +08:00
|
|
|
import { layoutSelectInput } from '../../layout/context';
|
|
|
|
import { Input } from '../../layout/layoutTypes';
|
|
|
|
import { TIMER_START, TIMER_STOP } from '../mutations';
|
|
|
|
import useTimer from '/imports/ui/core/hooks/useTImer';
|
2023-09-25 21:04:28 +08:00
|
|
|
|
2024-03-07 01:28:18 +08:00
|
|
|
const CDN = window.meetingClientSettings.public.app.cdn;
|
|
|
|
const BASENAME = window.meetingClientSettings.public.app.basename;
|
2023-09-25 21:04:28 +08:00
|
|
|
const HOST = CDN + BASENAME;
|
2024-03-07 01:28:18 +08:00
|
|
|
const trackName = window.meetingClientSettings.public.timer.music;
|
2023-09-25 21:04:28 +08:00
|
|
|
|
|
|
|
interface TimerIndicatorProps {
|
|
|
|
passedTime: number;
|
|
|
|
stopwatch: boolean;
|
|
|
|
songTrack: string;
|
|
|
|
running: boolean;
|
|
|
|
isModerator: boolean;
|
|
|
|
sidebarNavigationIsOpen: boolean;
|
|
|
|
sidebarContentIsOpen: boolean;
|
2024-03-01 01:29:46 +08:00
|
|
|
startedOn: number;
|
2023-09-25 21:04:28 +08:00
|
|
|
}
|
|
|
|
|
2024-03-07 01:28:18 +08:00
|
|
|
type ObjectKey = keyof typeof trackName;
|
|
|
|
|
2023-09-25 21:04:28 +08:00
|
|
|
const TimerIndicator: React.FC<TimerIndicatorProps> = ({
|
|
|
|
passedTime,
|
|
|
|
stopwatch,
|
|
|
|
songTrack,
|
|
|
|
running,
|
|
|
|
isModerator,
|
|
|
|
sidebarNavigationIsOpen,
|
|
|
|
sidebarContentIsOpen,
|
2024-03-01 01:29:46 +08:00
|
|
|
startedOn,
|
2023-09-25 21:04:28 +08:00
|
|
|
}) => {
|
|
|
|
const [time, setTime] = useState<number>(0);
|
|
|
|
const timeRef = useRef<HTMLSpanElement>(null);
|
|
|
|
const intervalRef = useRef<ReturnType<typeof setInterval>>();
|
|
|
|
const alarm = useRef<HTMLAudioElement>();
|
|
|
|
const music = useRef<HTMLAudioElement>();
|
|
|
|
const triggered = useRef<boolean>(true);
|
|
|
|
const alreadyNotified = useRef<boolean>(false);
|
2024-01-17 19:29:19 +08:00
|
|
|
const [startTimerMutation] = useMutation(TIMER_START);
|
|
|
|
const [stopTimerMutation] = useMutation(TIMER_STOP);
|
2024-03-01 01:54:59 +08:00
|
|
|
const [songTrackState, setSongTrackState] = useState<string>(songTrack);
|
2024-01-17 19:29:19 +08:00
|
|
|
|
|
|
|
const startTimer = () => {
|
|
|
|
startTimerMutation();
|
|
|
|
};
|
|
|
|
|
|
|
|
const stopTimer = () => {
|
2024-01-20 00:36:20 +08:00
|
|
|
stopTimerMutation();
|
2024-01-17 19:29:19 +08:00
|
|
|
};
|
2023-09-25 21:04:28 +08:00
|
|
|
|
|
|
|
useEffect(() => {
|
2024-03-01 01:54:59 +08:00
|
|
|
if (songTrackState !== songTrack) {
|
|
|
|
if (music.current) music.current.pause();
|
|
|
|
}
|
2023-09-25 21:04:28 +08:00
|
|
|
if (songTrack in trackName) {
|
2024-03-07 01:28:18 +08:00
|
|
|
music.current = new Audio(`${HOST}/resources/sounds/${trackName[songTrack as ObjectKey]}.mp3`);
|
2024-03-01 01:54:59 +08:00
|
|
|
setSongTrackState(songTrack);
|
2023-09-25 21:04:28 +08:00
|
|
|
music.current.addEventListener('timeupdate', () => {
|
|
|
|
const buffer = 0.19;
|
|
|
|
// Start playing the music before it ends to make the loop gapless
|
|
|
|
if (!music.current) return null;
|
|
|
|
if (music.current.currentTime > music.current.duration - buffer) {
|
|
|
|
music.current.currentTime = 0;
|
|
|
|
music.current.play();
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
|
|
if (music.current) music.current.pause();
|
|
|
|
};
|
2024-03-01 01:54:59 +08:00
|
|
|
}, [songTrack]);
|
|
|
|
|
2024-04-23 21:46:46 +08:00
|
|
|
useEffect(() => {
|
|
|
|
setTime(passedTime);
|
|
|
|
}, []);
|
|
|
|
|
2024-03-01 01:54:59 +08:00
|
|
|
useEffect(() => {
|
|
|
|
alarm.current = new Audio(`${HOST}/resources/sounds/alarm.mp3`);
|
2023-09-25 21:04:28 +08:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (running) {
|
|
|
|
setTime(passedTime);
|
|
|
|
intervalRef.current = setInterval(() => {
|
|
|
|
setTime((prev) => {
|
|
|
|
if (stopwatch) return (Math.round(prev / 1000) * 1000) + 1000;
|
|
|
|
const t = (Math.floor(prev / 1000) * 1000) - 1000;
|
|
|
|
if (t <= 0) {
|
|
|
|
if (!alreadyNotified.current) {
|
|
|
|
triggered.current = false;
|
|
|
|
alreadyNotified.current = true;
|
|
|
|
if (alarm.current) alarm.current.play();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return t < 0 ? 0 : t;
|
|
|
|
});
|
|
|
|
}, 1000);
|
|
|
|
} else if (!running) {
|
|
|
|
clearInterval(intervalRef.current);
|
|
|
|
}
|
|
|
|
}, [running]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
2024-01-19 04:20:28 +08:00
|
|
|
if (!running) return;
|
|
|
|
|
2024-01-18 22:09:13 +08:00
|
|
|
const timePassed = passedTime >= 0 ? passedTime : 0;
|
|
|
|
|
2023-09-25 21:04:28 +08:00
|
|
|
setTime((prev) => {
|
2024-01-18 22:09:13 +08:00
|
|
|
if (timePassed < prev) return timePassed;
|
|
|
|
if (timePassed > prev) return timePassed;
|
2023-09-25 21:04:28 +08:00
|
|
|
return prev;
|
|
|
|
});
|
2024-03-01 01:29:46 +08:00
|
|
|
}, [passedTime, stopwatch, startedOn]);
|
2023-09-25 21:04:28 +08:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!timeRef.current) {
|
|
|
|
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
|
|
if (music.current) music.current.pause();
|
|
|
|
if (alarm.current) alarm.current.pause();
|
|
|
|
}
|
|
|
|
}, [time]);
|
|
|
|
|
2024-03-01 01:54:59 +08:00
|
|
|
useEffect(() => {
|
2024-03-01 21:50:39 +08:00
|
|
|
if (running && songTrack !== 'noTrack') {
|
2024-03-01 01:54:59 +08:00
|
|
|
if (music.current) music.current.play();
|
2024-03-01 21:50:39 +08:00
|
|
|
} else if (!running || songTrack === 'noTrack') {
|
2024-03-01 01:54:59 +08:00
|
|
|
if (music.current) music.current.pause();
|
|
|
|
}
|
|
|
|
if (running && alreadyNotified.current) {
|
|
|
|
alreadyNotified.current = false;
|
|
|
|
}
|
|
|
|
}, [running, songTrackState]);
|
|
|
|
|
2023-10-10 22:18:01 +08:00
|
|
|
useEffect(() => {
|
2024-03-01 01:29:46 +08:00
|
|
|
if (startedOn === 0) {
|
2023-10-10 22:18:01 +08:00
|
|
|
setTime(passedTime);
|
|
|
|
}
|
2024-03-01 01:29:46 +08:00
|
|
|
}, [startedOn]);
|
2023-10-10 22:18:01 +08:00
|
|
|
|
2023-09-25 21:04:28 +08:00
|
|
|
const onClick = running ? stopTimer : startTimer;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Styled.TimerWrapper>
|
|
|
|
<Styled.Timer>
|
|
|
|
<Styled.TimerButton
|
|
|
|
running={running}
|
|
|
|
disabled={!isModerator}
|
|
|
|
hide={sidebarNavigationIsOpen && sidebarContentIsOpen}
|
|
|
|
role="button"
|
|
|
|
tabIndex={0}
|
|
|
|
onClick={isModerator ? onClick : () => {}}
|
2023-11-15 21:27:59 +08:00
|
|
|
data-test="timeIndicator"
|
2023-09-25 21:04:28 +08:00
|
|
|
>
|
|
|
|
<Styled.TimerContent>
|
|
|
|
<Styled.TimerIcon>
|
|
|
|
<Icon iconName="time" />
|
|
|
|
</Styled.TimerIcon>
|
|
|
|
<Styled.TimerTime
|
|
|
|
aria-hidden
|
|
|
|
ref={timeRef}
|
|
|
|
>
|
|
|
|
{humanizeSeconds(Math.floor(time / 1000))}
|
|
|
|
</Styled.TimerTime>
|
|
|
|
</Styled.TimerContent>
|
|
|
|
</Styled.TimerButton>
|
|
|
|
</Styled.Timer>
|
|
|
|
</Styled.TimerWrapper>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const TimerIndicatorContainer: React.FC = () => {
|
2023-11-14 02:10:41 +08:00
|
|
|
const { data: currentUser } = useCurrentUser((u) => ({
|
2023-09-25 21:04:28 +08:00
|
|
|
isModerator: u.isModerator,
|
|
|
|
}));
|
|
|
|
|
|
|
|
const {
|
|
|
|
data: timerData,
|
2024-04-30 23:45:05 +08:00
|
|
|
} = useTimer();
|
2023-09-25 21:04:28 +08:00
|
|
|
|
|
|
|
const [timeSync] = useTimeSync();
|
|
|
|
|
|
|
|
const sidebarNavigation = layoutSelectInput((i: Input) => i.sidebarNavigation);
|
|
|
|
const sidebarContent = layoutSelectInput((i: Input) => i.sidebarContent);
|
|
|
|
const sidebarNavigationIsOpen = sidebarNavigation.isOpen;
|
|
|
|
const sidebarContentIsOpen = sidebarContent.isOpen;
|
|
|
|
|
2024-04-30 23:45:05 +08:00
|
|
|
const currentTimer = timerData;
|
2024-01-19 04:20:28 +08:00
|
|
|
if (!currentTimer?.active) return null;
|
2023-09-25 21:04:28 +08:00
|
|
|
|
|
|
|
const {
|
|
|
|
accumulated,
|
|
|
|
running,
|
2024-03-01 01:29:46 +08:00
|
|
|
startedOn,
|
2023-09-25 21:04:28 +08:00
|
|
|
stopwatch,
|
|
|
|
songTrack,
|
|
|
|
time,
|
|
|
|
} = currentTimer;
|
|
|
|
const currentDate: Date = new Date();
|
2024-03-01 01:29:46 +08:00
|
|
|
const startedAtDate: Date = new Date(startedOn || Date.now());
|
2023-09-25 21:04:28 +08:00
|
|
|
const adjustedCurrent: Date = new Date(currentDate.getTime() + timeSync);
|
|
|
|
const timeDifferenceMs: number = adjustedCurrent.getTime() - startedAtDate.getTime();
|
|
|
|
|
|
|
|
const timePassed = stopwatch ? (
|
2024-04-30 23:45:05 +08:00
|
|
|
Math.floor(((running ? timeDifferenceMs : 0) + (accumulated ?? 0)))
|
2023-09-25 21:04:28 +08:00
|
|
|
) : (
|
2024-04-30 23:45:05 +08:00
|
|
|
Math.floor(((time ?? 0) - ((accumulated ?? 0) + (running ? timeDifferenceMs : 0)))));
|
2023-09-25 21:04:28 +08:00
|
|
|
|
|
|
|
return (
|
|
|
|
<TimerIndicator
|
2024-01-18 22:09:13 +08:00
|
|
|
passedTime={timePassed}
|
2024-04-30 23:45:05 +08:00
|
|
|
stopwatch={stopwatch ?? false}
|
|
|
|
songTrack={songTrack ?? 'noTrack'}
|
|
|
|
running={running ?? false}
|
|
|
|
isModerator={currentUser?.isModerator ?? false}
|
2023-09-25 21:04:28 +08:00
|
|
|
sidebarNavigationIsOpen={sidebarNavigationIsOpen}
|
|
|
|
sidebarContentIsOpen={sidebarContentIsOpen}
|
2024-04-30 23:45:05 +08:00
|
|
|
startedOn={startedOn ?? 0}
|
2023-09-25 21:04:28 +08:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default TimerIndicatorContainer;
|