From 097b345c92a4c0b1d04090a6c41d38cad03724ef Mon Sep 17 00:00:00 2001 From: Jeff Davidson Date: Sun, 5 Jan 2025 14:27:43 -0800 Subject: [PATCH] Add guardrails for duplicate/excess answers in no-guess-queue mode. These guardrails exist for guess-queue mode but were missing from no-guess-queue mode. This change brings parity between the two modes. Note that: 1. This is not a firm guarantee against (unintended) duplicate answers, since two submissions made at ~the same time won't detect the presence of the other submission in time. Only a read-modify-write transaction would prevent this. 2. If duplicate answers _are_ submitted, clearing one answer in no-guess-queue mode will mark the puzzle as incorrect and clear the answer, while leaving a correct guess in place. This is an inconsistent state. The workaround is to clear all copies of the answer and resubmit it. --- imports/client/components/PuzzlePage.tsx | 45 ++++++++++++++++++++---- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/imports/client/components/PuzzlePage.tsx b/imports/client/components/PuzzlePage.tsx index c355cd473..cc2895bb9 100644 --- a/imports/client/components/PuzzlePage.tsx +++ b/imports/client/components/PuzzlePage.tsx @@ -1649,6 +1649,8 @@ const PuzzleAnswerModal = React.forwardRef( forwardedRef: React.Ref, ) => { const [answer, setAnswer] = useState(""); + const [confirmingSubmit, setConfirmingSubmit] = useState(false); + const [confirmationMessage, setConfirmationMessage] = useState(""); const [submitState, setSubmitState] = useState( PuzzleAnswerSubmitState.IDLE, ); @@ -1675,6 +1677,7 @@ const PuzzleAnswerModal = React.forwardRef( const onAnswerChange: NonNullable = useCallback((e) => { setAnswer(e.currentTarget.value); + setConfirmingSubmit(false); }, []); const onDismissError = useCallback(() => { @@ -1682,7 +1685,30 @@ const PuzzleAnswerModal = React.forwardRef( setSubmitError(""); }, []); + const solvedness = useMemo(() => { + return computeSolvedness(puzzle); + }, [puzzle]); + const onSubmit = useCallback(() => { + const strippedAnswer = answer.replaceAll(/\s/g, ""); + const repeatAnswer = puzzle.answers.find((a) => { + return a.replaceAll(/\s/g, "") === strippedAnswer; + }); + if ((repeatAnswer || solvedness !== "unsolved") && !confirmingSubmit) { + const repeatAnswerStr = repeatAnswer + ? "This answer has already been submitted. " + : ""; + const solvednessStr = { + solved: "This puzzle has already been solved. ", + noAnswers: + "This puzzle does not expect any answers to be submitted. ", + unsolved: "", + }[solvedness]; + const msg = `${solvednessStr} ${repeatAnswerStr} Are you sure you want to submit this guess?`; + setConfirmationMessage(msg); + setConfirmingSubmit(true); + return; + } setSubmitState(PuzzleAnswerSubmitState.SUBMITTING); setSubmitError(""); addPuzzleAnswer.call( @@ -1699,20 +1725,24 @@ const PuzzleAnswerModal = React.forwardRef( setSubmitState(PuzzleAnswerSubmitState.IDLE); hide(); } + setConfirmingSubmit(false); }, ); - }, [puzzle._id, answer, hide]); + }, [ + puzzle._id, + puzzle.answers, + confirmingSubmit, + solvedness, + answer, + hide, + ]); return ( @@ -1730,6 +1760,9 @@ const PuzzleAnswerModal = React.forwardRef( + {confirmingSubmit ? ( + {confirmationMessage} + ) : null} {submitState === PuzzleAnswerSubmitState.FAILED ? ( {submitError ||