Skip to content

Commit

Permalink
Fix: keep track of question index in timer so client knows when to re…
Browse files Browse the repository at this point in the history
…start it.
  • Loading branch information
dleclercpro committed Jun 10, 2024
1 parent ff0a806 commit e5a559b
Show file tree
Hide file tree
Showing 10 changed files with 68 additions and 48 deletions.
2 changes: 1 addition & 1 deletion Apps/Client/src/components/overlays/AnswerOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const AnswerOverlay: React.FC = () => {
const user = useUser();

const appQuestionIndex = app.questionIndex;
const nextAppQuestionIndex = appQuestionIndex + 1;
const nextAppQuestionIndex = appQuestionIndex + 1; // FIXME: 19/18?

const question = useQuestion(appQuestionIndex);
const overlay = useOverlay(OverlayName.Answer);
Expand Down
4 changes: 0 additions & 4 deletions Apps/Client/src/hooks/useQuiz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { Language, NO_QUESTION_INDEX, NO_VOTE_INDEX, UserType } from '../constan
import { useTranslation } from 'react-i18next';
import useApp from './useApp';
import { setQuestionIndex } from '../reducers/AppReducer';
import { DEBUG } from '../config';
import { sleep } from '../utils/time';
import TimeDuration from '../models/TimeDuration';
import { TimeUnit } from '../types/TimeTypes';
Expand Down Expand Up @@ -72,9 +71,6 @@ const useQuiz = () => {

// Force user to go to next question according to setting
if (status.isNextQuestionForced && app.questionIndex !== questionIndex) {
if (DEBUG) {
console.log(`Forcing user to next question: #${questionIndex + 1}`);
}
dispatch(setQuestionIndex(questionIndex));
}
}, [status]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import TimeDuration from '../models/TimeDuration';
import { TimeUnit } from '../types/TimeTypes';
import useCountdownTimer from './useCountdownTimer';
import useTimer from './useTimer';
import { NO_TIME } from '../constants';
import useQuiz from './useQuiz';

const useServerCountdownTimer = () => {
const useServerTimer = (interval: TimeDuration = new TimeDuration(1, TimeUnit.Second)) => {
const quiz = useQuiz();
const data = quiz.status?.timer;

Expand All @@ -20,8 +20,8 @@ const useServerCountdownTimer = () => {
remainingTime = NO_TIME;
}

const timer = useCountdownTimer({
interval: new TimeDuration(1, TimeUnit.Second),
const localTimer = useTimer({
interval,
duration: remainingTime,
autoStart: false,
});
Expand All @@ -30,13 +30,13 @@ const useServerCountdownTimer = () => {
isEnabled,
duration,
startedAt,
isRunning: timer.isRunning,
isDone: timer.isDone,
time: timer.time,
start: timer.start,
stop: timer.stop,
restart: timer.restart,
isRunning: localTimer.isRunning,
isDone: localTimer.isDone,
time: localTimer.time,
start: localTimer.start,
stop: localTimer.stop,
restart: localTimer.restart,
};
};

export default useServerCountdownTimer;
export default useServerTimer;
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ interface TimerOptions {
autoStart?: boolean; // Whether to start the timer immediately
}

const useCountdownTimer = ({ duration, interval = new TimeDuration(1, TimeUnit.Second), autoStart = false }: TimerOptions) => {
const [time, setTime] = useState(NO_TIME);
const useTimer = ({ duration, interval = new TimeDuration(1, TimeUnit.Second), autoStart = false }: TimerOptions) => {
const [time, setTime] = useState(duration);

const [isRunning, setIsRunning] = useState(autoStart);
const [isDone, setIsDone] = useState(false);
const timerRef = useRef<NodeJS.Timeout | null>(null);
Expand All @@ -30,7 +31,7 @@ const useCountdownTimer = ({ duration, interval = new TimeDuration(1, TimeUnit.S
setIsRunning(true);
setIsDone(false);
}
}, [isRunning, isDone, duration]);
}, [isRunning, duration]);



Expand All @@ -49,11 +50,10 @@ const useCountdownTimer = ({ duration, interval = new TimeDuration(1, TimeUnit.S


const restart = useCallback(() => {
if (isRunning) {
stop();
}
stop();
start();
}, [isRunning, stop, start]);

}, [stop, start]);



Expand Down Expand Up @@ -85,13 +85,15 @@ const useCountdownTimer = ({ duration, interval = new TimeDuration(1, TimeUnit.S
timerRef.current = null;
}
};
}, [isRunning, interval]);
}, [isRunning, time, interval]);



// Handle timer stop (clean up interval)
useEffect(() => {
if (!isRunning && timerRef.current) {
console.warn(`Inconsistent state in timer. Resetting it...`);

setIsRunning(false);
setIsDone(false);

Expand All @@ -114,4 +116,4 @@ const useCountdownTimer = ({ duration, interval = new TimeDuration(1, TimeUnit.S
return { time, isRunning, isDone, start, stop, restart };
};

export default useCountdownTimer;
export default useTimer;
4 changes: 2 additions & 2 deletions Apps/Client/src/models/TimeDuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ class TimeDuration implements Comparable {
return this.toMs().getAmount();
}

public static deserialize(str: string) {
return new TimeDuration(parseInt(str, 10), TimeUnit.Millisecond);
public static deserialize(amount: number) {
return new TimeDuration(amount, TimeUnit.Millisecond);
}

public isZero() {
Expand Down
49 changes: 32 additions & 17 deletions Apps/Client/src/pages/QuizPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import QuestionForm from '../components/forms/QuestionForm';
import { REFRESH_STATUS_INTERVAL } from '../config';
import AdminQuizForm from '../components/forms/AdminQuizForm';
import { useTranslation } from 'react-i18next';
import { AspectRatio, Language, QuestionType } from '../constants';
import { AspectRatio, Language, NO_QUESTION_INDEX, QuestionType } from '../constants';
import Page from './Page';
import useServerCountdownTimer from '../hooks/useServerCountdownTimer';
import useQuiz from '../hooks/useQuiz';
import useUser from '../hooks/useUser';
import useApp from '../hooks/useApp';
import useOverlay from '../hooks/useOverlay';
import { OverlayName } from '../reducers/OverlaysReducer';
import useQuestion from '../hooks/useQuestion';
import useServerTimer from '../hooks/useServerTimer';

const QuizPage: React.FC = () => {
const { t, i18n } = useTranslation();
Expand All @@ -28,10 +28,13 @@ const QuizPage: React.FC = () => {
const answerOverlay = useOverlay(OverlayName.Answer);
const lobbyOverlay = useOverlay(OverlayName.Lobby);

const timer = useServerCountdownTimer();
const timer = useServerTimer();

const [choice, setChoice] = useState('');

const hasTimerQuestionIndex = quiz.status?.timer?.questionIndex !== undefined;
const timerQuestionIndex = hasTimerQuestionIndex ? quiz.status!.timer!.questionIndex : NO_QUESTION_INDEX;

const isReady = quiz.id !== null && quiz.questions !== null && quiz.status !== null && question !== null && question.data !== null;


Expand Down Expand Up @@ -77,12 +80,7 @@ const QuizPage: React.FC = () => {
return;
}

quiz.refreshStatusPlayersAndScores()
.then(() => {
if (timer.isEnabled) {
timer.restart();
}
});
quiz.refreshStatusPlayersAndScores();

}, [app.questionIndex]);

Expand All @@ -108,10 +106,10 @@ const QuizPage: React.FC = () => {

if (choice !== question.answer.chosen.value) {
setChoice(question.answer.chosen.value);
}

if (!answerOverlay.isOpen) {
answerOverlay.open();
if (!answerOverlay.isOpen) {
answerOverlay.open();
}
}

}, [question.answer.chosen]);
Expand All @@ -128,22 +126,39 @@ const QuizPage: React.FC = () => {



// FIXME: what if every user is answering at their own pace? Then server timer does not make sense!
// Start timer if enabled
const shouldStartTimer = timer.isEnabled && !timer.isRunning && quiz.isStarted;
const shouldStartTimer = quiz.isStarted && timer.isEnabled && !timer.isRunning && !timer.isDone;

useEffect(() => {
if (shouldStartTimer) {
timer.start();
timer.start();
}
}, [shouldStartTimer]);



// Restart timer when moving to next question
const shouldRestartTimer = quiz.isStarted && timer.isEnabled && timerQuestionIndex !== NO_QUESTION_INDEX;

useEffect(() => {
if (shouldRestartTimer) {
timer.restart();
}
}, [timerQuestionIndex]);



// FIXME: close answer overlay when timer is up for current question!
// Show answer once timer has expired
const shouldShowAnswer = timer.isEnabled && quiz.isStarted && timer.isDone;
const shouldShowAnswer = timer.isEnabled && timer.isDone && quiz.isStarted;
useEffect(() => {
if (shouldShowAnswer) {
if (shouldShowAnswer && !answerOverlay.isOpen) {
answerOverlay.open();
}
if (!shouldShowAnswer && answerOverlay.isOpen) {
answerOverlay.close();
}

}, [shouldShowAnswer]);

Expand Down Expand Up @@ -179,7 +194,7 @@ const QuizPage: React.FC = () => {
)}
{quiz.isStarted && (
<QuestionForm
remainingTime={timer.isEnabled && (timer.isRunning || timer.isDone) ? timer.time : undefined}
remainingTime={timer.isEnabled ? timer.time : undefined}
index={question.index}
topic={question.data!.topic}
question={question.data!.question}
Expand Down
1 change: 1 addition & 0 deletions Apps/Client/src/types/DataTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type AnswerData = {
};

export type TimerData = {
questionIndex: number,
startedAt: Date,
duration: {
amount: number,
Expand Down
8 changes: 4 additions & 4 deletions Apps/Server/src/controllers/quiz/StartQuestionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,16 @@ const StartQuestionController: RequestHandler = async (req, res, next) => {
throw new InvalidQuestionIndexError();
}

// We can finally move on to next question
await quiz.setQuestionIndex(questionIndex);
logger.info(`Question ${questionIndex + 1}/${questionCount} of quiz '${quiz.getId()}' has been started by admin '${username}'.`);

// Restart timer for new question
if (quiz.isTimed()) {
await quiz.restartTimer();
logger.debug(`Restarted timer of quiz (ID = ${quiz.getId()}).`);
}

// We can finally move on to next question
await quiz.setQuestionIndex(questionIndex);
logger.info(`Question ${questionIndex + 1}/${questionCount} of quiz '${quiz.getId()}' has been started by admin '${username}'.`);

return res.json(successResponse());

} catch (err: any) {
Expand Down
5 changes: 5 additions & 0 deletions Apps/Server/src/models/Quiz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class Quiz {
...this.status,
...(this.status.timer ? {
timer: {
questionIndex: this.status.timer.questionIndex,
startedAt: this.status.timer.startedAt!.toUTCString(),
duration: this.status.timer.duration,
},
Expand All @@ -76,6 +77,7 @@ class Quiz {
...quiz.status,
...(quiz.status.timer ? {
timer: {
questionIndex: quiz.status.timer.questionIndex,
startedAt: new Date(quiz.status.timer.startedAt),
duration: quiz.status.timer.duration,
},
Expand Down Expand Up @@ -162,6 +164,7 @@ class Quiz {
// Create a timer
if (isTimed) {
this.status.timer = {
questionIndex: this.getQuestionIndex(),
startedAt: new Date(),
duration: {
amount: TIMER_DURATION.getAmount(),
Expand Down Expand Up @@ -228,6 +231,8 @@ class Quiz {
throw new Error('MISSING_TIMER');
}

// Assign question index to timer so client app knows when a new timer has been created
this.status.timer!.questionIndex = this.getQuestionIndex();
this.status.timer!.startedAt = new Date();

await this.save();
Expand Down
1 change: 1 addition & 0 deletions Apps/Server/src/types/DataTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type AnswerData = {
};

export type TimerData = {
questionIndex: number,
startedAt: Date,
duration: {
amount: number,
Expand Down

0 comments on commit e5a559b

Please sign in to comment.