From 1b1208c2318dd421a6ce71d8fd4fde664d607c7c 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 | 49 ++++++++++++++++++++---- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/imports/client/components/PuzzlePage.tsx b/imports/client/components/PuzzlePage.tsx index c355cd473..f9a7a0ef7 100644 --- a/imports/client/components/PuzzlePage.tsx +++ b/imports/client/components/PuzzlePage.tsx @@ -1084,7 +1084,11 @@ const PuzzlePageMetadata = ({ {" Answer"} {/* eslint-disable-next-line @typescript-eslint/no-use-before-define */} - + ); } @@ -1643,12 +1647,16 @@ const PuzzleAnswerModal = React.forwardRef( ( { puzzle, + guesses, }: { puzzle: PuzzleType; + guesses: GuessType[]; }, forwardedRef: React.Ref, ) => { const [answer, setAnswer] = useState(""); + const [confirmingSubmit, setConfirmingSubmit] = useState(false); + const [confirmationMessage, setConfirmationMessage] = useState(""); const [submitState, setSubmitState] = useState( PuzzleAnswerSubmitState.IDLE, ); @@ -1675,6 +1683,7 @@ const PuzzleAnswerModal = React.forwardRef( const onAnswerChange: NonNullable = useCallback((e) => { setAnswer(e.currentTarget.value); + setConfirmingSubmit(false); }, []); const onDismissError = useCallback(() => { @@ -1682,7 +1691,33 @@ const PuzzleAnswerModal = React.forwardRef( setSubmitError(""); }, []); + const solvedness = useMemo(() => { + return computeSolvedness(puzzle); + }, [puzzle]); + const onSubmit = useCallback(() => { + const strippedAnswer = answer.replaceAll(/\s/g, ""); + const repeatAnswer = guesses.find((g) => { + return ( + g.state === "correct" && + g.guess.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 answer?`; + setConfirmationMessage(msg); + setConfirmingSubmit(true); + return; + } setSubmitState(PuzzleAnswerSubmitState.SUBMITTING); setSubmitError(""); addPuzzleAnswer.call( @@ -1699,20 +1734,17 @@ const PuzzleAnswerModal = React.forwardRef( setSubmitState(PuzzleAnswerSubmitState.IDLE); hide(); } + setConfirmingSubmit(false); }, ); - }, [puzzle._id, answer, hide]); + }, [puzzle._id, confirmingSubmit, guesses, solvedness, answer, hide]); return ( @@ -1730,6 +1762,9 @@ const PuzzleAnswerModal = React.forwardRef( + {confirmingSubmit ? ( + {confirmationMessage} + ) : null} {submitState === PuzzleAnswerSubmitState.FAILED ? ( {submitError ||