From 277c54957b19bc2b6e4defd81a983026eb55017c Mon Sep 17 00:00:00 2001 From: Andrew Casal Date: Sat, 5 Oct 2024 15:56:50 -0400 Subject: [PATCH 1/7] Add tooltip to modal, POC Modal provider --- .../ChallengeModal/ChallengeModal.test.tsx | 64 ++++++++++++++++ .../ChallengeModal/ChallengeModal.tsx | 20 ++++- src/components/Modal/Modal.tsx | 3 +- src/components/Modal/ModalProvider.tsx | 74 +++++++++++++++++++ src/main.tsx | 3 +- src/views/HelpFlows/HelpFlows.tsx | 3 +- src/views/HelpFlows/ModalHelp.tsx | 32 ++++++++ src/views/Play/Play.tsx | 27 ++++--- 8 files changed, 211 insertions(+), 15 deletions(-) create mode 100644 src/components/ChallengeModal/ChallengeModal.test.tsx create mode 100644 src/components/Modal/ModalProvider.tsx create mode 100644 src/views/HelpFlows/ModalHelp.tsx diff --git a/src/components/ChallengeModal/ChallengeModal.test.tsx b/src/components/ChallengeModal/ChallengeModal.test.tsx new file mode 100644 index 0000000000..9bab0cec1b --- /dev/null +++ b/src/components/ChallengeModal/ChallengeModal.test.tsx @@ -0,0 +1,64 @@ +/* + * Copyright (C) Online-Go.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/* cspell: words groupadmin cotsen */ +import * as React from "react"; + +import * as ChallengeModal from "./ChallengeModal"; +import * as Modal from "./../Modal"; +import { render } from "@testing-library/react"; +import { ModalConsumer, ModalProvider, ModalTypes } from "../Modal/ModalProvider"; + +jest.mock("./../Modal", () => { + return { + ...jest.requireActual("./../Modal"), + }; +}); + +describe("ChallengeModal", () => { + it("should do a computer challenge via provider", () => { + const challengeModalSpy = jest + .spyOn(ChallengeModal, "ChallengeModal") + .mockImplementation(jest.fn()); + + render( + + + {({ showModal }) => { + showModal(ModalTypes.Challenge); + return
; + }} + + , + ); + expect(challengeModalSpy).toHaveBeenCalledWith( + expect.objectContaining({ + game_record_mode: true, + mode: "open", + }), + {}, + ); + }); + + it("should do a computer challenge via modal", () => { + const modalSpy = jest.spyOn(Modal, "openModal").mockReturnValue(true); + ChallengeModal.challengeComputer(); + expect(modalSpy).toHaveBeenCalledWith( + , + ); + }); +}); diff --git a/src/components/ChallengeModal/ChallengeModal.tsx b/src/components/ChallengeModal/ChallengeModal.tsx index bc94dd8edc..69827d3779 100644 --- a/src/components/ChallengeModal/ChallengeModal.tsx +++ b/src/components/ChallengeModal/ChallengeModal.tsx @@ -18,6 +18,7 @@ import * as React from "react"; import * as data from "@/lib/data"; import * as player_cache from "@/lib/player_cache"; +import * as DynamicHelp from "react-dynamic-help"; import { OgsResizeDetector } from "@/components/OgsResizeDetector"; import { browserHistory } from "@/lib/ogsHistory"; @@ -64,7 +65,7 @@ import { export type ChallengeDetails = rest_api.ChallengeDetails; -type ChallengeModes = "open" | "computer" | "player" | "demo"; +export type ChallengeModes = "open" | "computer" | "player" | "demo"; interface Events {} @@ -1778,7 +1779,22 @@ export class ChallengeModalBody extends React.Component<   {player_username} )} - {mode === "computer" && {_("Computer")}} + {mode === "computer" && ( + + {(value) => { + const { ref: modalHelpIntro } = + value.registerTargetItem("modal-help-intro"); + return ( + value.triggerFlow("modal-help")} + > + {_("Computer")} + + ); + }} + + )}
diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index a264b15bd5..bdb2cc897f 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -27,7 +27,7 @@ type ModalProps

= P & { fastDismiss?: boolean }; export type ModalConstructorInput

= ModalProps

| Readonly>; export class Modal extends TypedEventEmitterPureComponent< Events & { close: never; open: never }, - P & { fastDismiss?: boolean }, + P & { fastDismiss?: boolean; onClose?: () => void }, S > { constructor(props: ModalConstructorInput

) { @@ -39,6 +39,7 @@ export class Modal extends TypedEventEmitterPureComponent< } close = () => { + this.props.onClose && this.props.onClose(); this.emit("close"); }; bindContainer(container: HTMLElement) { diff --git a/src/components/Modal/ModalProvider.tsx b/src/components/Modal/ModalProvider.tsx new file mode 100644 index 0000000000..145e1b8959 --- /dev/null +++ b/src/components/Modal/ModalProvider.tsx @@ -0,0 +1,74 @@ +/* + * Copyright (C) Online-Go.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/* cspell: words groupadmin cotsen */ +import * as React from "react"; + +import { ChallengeModal, ChallengeModes } from "../ChallengeModal"; +import { createPortal } from "react-dom"; + +type ModalProviderType = { + showModal: (types: ModalTypes) => void; +}; + +export enum ModalTypes { + Challenge = "challenge", +} + +interface Modals { + challenge: { + mode: ChallengeModes; + playerId?: number; + initialState: any; + }; +} + +type Property = T[K]; + +const { Provider, Consumer } = React.createContext({} as ModalProviderType); + +export const ModalConsumer = Consumer; + +export const ModalProvider = ({ children }: React.PropsWithChildren): JSX.Element => { + const [modal, setModal] = React.useState(false); + const [props, setProps] = React.useState({} as Property); + + function showModal() { + setModal(true); + setProps({ + mode: "computer", + initialState: null, + playerId: undefined, + }); + } + + const hideModal = () => { + setModal(false); + }; + return ( + + {modal && + createPortal( +

+ +
, + document.body, + )} + {children} + + ); +}; diff --git a/src/main.tsx b/src/main.tsx index 3128c79383..f28935d865 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -328,7 +328,7 @@ const react_root = ReactDOM.createRoot(document.getElementById("main-content") a react_root.render( - {routes} + {routes} , @@ -339,4 +339,5 @@ window.preferences = preferences; window.player_cache = player_cache; import * as requests from "@/lib/requests"; +import { ModalProvider } from "./components/Modal/ModalProvider"; window.requests = requests; diff --git a/src/views/HelpFlows/HelpFlows.tsx b/src/views/HelpFlows/HelpFlows.tsx index b218c0cef1..5199cd3c9d 100644 --- a/src/views/HelpFlows/HelpFlows.tsx +++ b/src/views/HelpFlows/HelpFlows.tsx @@ -29,6 +29,7 @@ import { UndoRequestReceivedIntro } from "./UndoIntro"; import { CommunityModeratorIntro } from "./CommunityModeratorIntro"; import { OJEIntro } from "./OJEIntro"; import { GameLogHelp } from "./GameLogHelp"; +import { ModalHelp } from "./ModalHelp"; /** * This component is a handy wrapper for all the Help Flows, and reset on login/logout @@ -80,7 +81,7 @@ export function HelpFlows(): JSX.Element { - + diff --git a/src/views/HelpFlows/ModalHelp.tsx b/src/views/HelpFlows/ModalHelp.tsx new file mode 100644 index 0000000000..95c05fd773 --- /dev/null +++ b/src/views/HelpFlows/ModalHelp.tsx @@ -0,0 +1,32 @@ +/* + * Copyright (C) Online-Go.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/* cspell: words groupadmin cotsen */ + +import * as React from "react"; + +import { HelpFlow, HelpItem } from "react-dynamic-help"; + +export function ModalHelp(): JSX.Element { + return ( + + +
test
+
+
+ ); +} diff --git a/src/views/Play/Play.tsx b/src/views/Play/Play.tsx index 8913e9198f..e0f27b2188 100644 --- a/src/views/Play/Play.tsx +++ b/src/views/Play/Play.tsx @@ -57,6 +57,7 @@ import { ChallengeFilterKey, shouldDisplayChallenge, } from "@/lib/challenge_utils"; +import { ModalConsumer, ModalTypes } from "../../components/Modal/ModalProvider"; const CHALLENGE_LIST_FREEZE_PERIOD = 1000; // Freeze challenge list for this period while they move their mouse on it @@ -828,16 +829,22 @@ export class Play extends React.Component<{}, PlayState> {
- + + {({ showModal }) => { + return ( + + ); + }} + -
+ return ( +
+
+ {Object.keys(languages) + .sort(language_sorter) + .map((lc, idx) => ( + setLanguage(lc)} + > + {languages[lc]} + + ))}
- ); - } -} +
+ +
+
+ ); +}; diff --git a/src/components/Modal/ModalProvider.tsx b/src/components/Modal/ModalProvider.tsx index ebe7a211d7..a074c4c06b 100644 --- a/src/components/Modal/ModalProvider.tsx +++ b/src/components/Modal/ModalProvider.tsx @@ -20,15 +20,16 @@ import * as React from "react"; import { ChallengeModal, ChallengeModes } from "../ChallengeModal"; import { createPortal } from "react-dom"; -import { deepEqual } from "@/lib/misc"; +import { LanguagePickerModal } from "../LanguagePicker"; type ModalProviderType = { - showModal: (types: ModalTypes) => void; + showModal: (type: ModalTypes) => void; hideModal: () => void; }; export enum ModalTypes { Challenge = "challenge", + LanguagePicker = "languagePicker", } interface Modals { @@ -39,7 +40,9 @@ interface Modals { }; } -type Property = T[K]; +type ModalTypesProps = { + [key: string]: any; +}; export const ModalContext = React.createContext({} as ModalProviderType); const { Provider, Consumer } = ModalContext; @@ -47,32 +50,55 @@ const { Provider, Consumer } = ModalContext; export const ModalConsumer = Consumer; export const ModalProvider = ({ children }: React.PropsWithChildren): JSX.Element => { - const [modal, setModal] = React.useState(false); - const [props, setProps] = React.useState({} as Property); + const [modalType, setModalType] = React.useState(null as ModalTypes | null); + const [modalProps, setModalProps] = React.useState({} as ModalTypesProps); - function showModal() { - setModal(true); + const showModal = (type: ModalTypes) => { + setModalType(type); - const payload = { - mode: "computer" as ChallengeModes, - initialState: null, - playerId: undefined, + switch (type) { + case ModalTypes.Challenge: + setModalProps({ + mode: "computer" as ChallengeModes, + initialState: null, + playerId: undefined, + }); + break; + default: + break; + } + }; + + const hideModal = () => { + setModalType(null); + }; + + React.useEffect(() => { + const handleEscape = (event: KeyboardEvent) => { + if (event.key === "Escape" && modalType) { + hideModal(); + } }; - if (!deepEqual(props, payload)) { - setProps(payload); - } - } + document.addEventListener("keydown", handleEscape); + + return () => { + document.removeEventListener("keydown", handleEscape); + }; + }, [modalType, hideModal]); - function hideModal() { - setModal(false); - } return ( - {modal && + {modalType && createPortal(
- + {modalType === ModalTypes.Challenge && ( + + )} + {modalType === ModalTypes.LanguagePicker && }
, document.body, )} diff --git a/src/views/Play/Play.tsx b/src/views/Play/Play.tsx index e0f27b2188..abaaf9f6b1 100644 --- a/src/views/Play/Play.tsx +++ b/src/views/Play/Play.tsx @@ -35,7 +35,7 @@ import { timeControlSystemText, usedForCheating, } from "@/components/TimeControl"; -import { challenge, challengeComputer } from "@/components/ChallengeModal"; +import { challenge } from "@/components/ChallengeModal"; import { openGameAcceptModal } from "@/components/GameAcceptModal"; import { errorAlerter, rulesText, dup, uuid, ignore } from "@/lib/misc"; import { Player } from "@/components/Player"; @@ -380,14 +380,6 @@ export class Play extends React.Component<{}, PlayState> { this.forceUpdate(); }; - newComputerGame = () => { - if (bot_count() === 0) { - void alert.fire(_("Sorry, all bots seem to be offline, please try again later.")); - return; - } - challengeComputer(); - }; - newCustomGame = () => { challenge(undefined, undefined, undefined, undefined, this.challengeCreated); }; @@ -834,7 +826,17 @@ export class Play extends React.Component<{}, PlayState> { return ( - - + return ( +
+
+

{_("Player to challenge")}:

- ); - } -} - -export function openForkModal(goban: GobanRenderer) { - return openModal(); -} -export function challengeFromBoardPosition(goban: GobanRenderer) { - if (!goban) { - return; - } - - openForkModal(goban); -} +
+ +
+
+ + +
+
+ ); +}; diff --git a/src/components/Modal/ModalProvider.tsx b/src/components/Modal/ModalProvider.tsx index a074c4c06b..8b92c1b668 100644 --- a/src/components/Modal/ModalProvider.tsx +++ b/src/components/Modal/ModalProvider.tsx @@ -21,15 +21,18 @@ import * as React from "react"; import { ChallengeModal, ChallengeModes } from "../ChallengeModal"; import { createPortal } from "react-dom"; import { LanguagePickerModal } from "../LanguagePicker"; +import { ForkModal } from "../ChallengeModal/ForkModal"; +import { GobanRenderer } from "goban"; type ModalProviderType = { - showModal: (type: ModalTypes) => void; + showModal: (type: ModalTypes, props?: ModalTypesProps) => void; hideModal: () => void; }; export enum ModalTypes { Challenge = "challenge", LanguagePicker = "languagePicker", + Fork = "fork", } interface Modals { @@ -38,6 +41,9 @@ interface Modals { playerId?: number; initialState: any; }; + fork: { + goban: GobanRenderer; + }; } type ModalTypesProps = { @@ -53,7 +59,7 @@ export const ModalProvider = ({ children }: React.PropsWithChildren): JSX.Elemen const [modalType, setModalType] = React.useState(null as ModalTypes | null); const [modalProps, setModalProps] = React.useState({} as ModalTypesProps); - const showModal = (type: ModalTypes) => { + const showModal = (type: ModalTypes, props?: ModalTypesProps) => { setModalType(type); switch (type) { @@ -64,6 +70,11 @@ export const ModalProvider = ({ children }: React.PropsWithChildren): JSX.Elemen playerId: undefined, }); break; + case ModalTypes.Fork: + setModalProps({ + goban: (props as Modals["fork"]).goban, + }); + break; default: break; } @@ -99,6 +110,9 @@ export const ModalProvider = ({ children }: React.PropsWithChildren): JSX.Elemen /> )} {modalType === ModalTypes.LanguagePicker && } + {modalType === ModalTypes.Fork && ( + + )} , document.body, )} diff --git a/src/views/Game/GameDock.tsx b/src/views/Game/GameDock.tsx index b3d490e6fc..63d6f88136 100644 --- a/src/views/Game/GameDock.tsx +++ b/src/views/Game/GameDock.tsx @@ -30,7 +30,6 @@ import { openGameLinkModal } from "./GameLinkModal"; import { openGameLogModal } from "./GameLogModal"; import { sfx } from "@/lib/sfx"; import { alert } from "@/lib/swal_config"; -import { challengeFromBoardPosition } from "@/components/ChallengeModal/ForkModal"; import { errorAlerter } from "@/lib/misc"; import { doAnnul } from "@/lib/moderation"; import { openReport } from "@/components/Report"; @@ -39,6 +38,7 @@ import { openGameInfoModal } from "./GameInfoModal"; import { useUserIsParticipant } from "./GameHooks"; import { useGoban } from "./goban_context"; import { Tooltip } from "../../components/Tooltip"; +import { ModalConsumer, ModalTypes } from "@/components/Modal/ModalProvider"; interface DockProps { annulled: boolean; @@ -139,11 +139,6 @@ export function GameDock({ } }; - const fork = () => { - if (!user.anonymous && !engine.rengo && !goban.isAnalysisDisabled()) { - challengeFromBoardPosition(goban); - } - }; const showLinkModal = () => { openGameLinkModal(goban); }; @@ -446,16 +441,34 @@ export function GameDock({ - - {_("Fork game")} - + + {({ showModal }) => ( + { + if ( + !user.anonymous && + !engine.rengo && + !goban.isAnalysisDisabled() + ) { + if (!goban) { + return; + } + + return showModal(ModalTypes.Fork, { + goban, + }); + } + }} + className={ + user.anonymous || engine.rengo || goban.isAnalysisDisabled() + ? "disabled" + : "" + } + > + {_("Fork game")} + + )} + From 58d1d874926e45343d2c2beb17f58019a21891c9 Mon Sep 17 00:00:00 2001 From: Andrew Casal Date: Thu, 24 Oct 2024 13:44:41 -0400 Subject: [PATCH 6/7] add registry, separate files --- .../ChallengeModal/ChallengeModal.test.tsx | 11 ++--- src/components/ChallengeModal/ForkModal.tsx | 2 +- .../LanguagePicker/LanguagePicker.tsx | 10 ++--- src/components/Modal/ModalContext.tsx | 36 ++++++++++++++++ src/components/Modal/ModalProvider.tsx | 42 +++++-------------- src/components/Modal/ModalRegistry.ts | 38 +++++++++++++++++ src/views/Game/GameDock.tsx | 6 +-- src/views/Play/Play.tsx | 6 +-- 8 files changed, 102 insertions(+), 49 deletions(-) create mode 100644 src/components/Modal/ModalContext.tsx create mode 100644 src/components/Modal/ModalRegistry.ts diff --git a/src/components/ChallengeModal/ChallengeModal.test.tsx b/src/components/ChallengeModal/ChallengeModal.test.tsx index b59d8ba946..b95c98ad7e 100644 --- a/src/components/ChallengeModal/ChallengeModal.test.tsx +++ b/src/components/ChallengeModal/ChallengeModal.test.tsx @@ -20,7 +20,8 @@ import * as React from "react"; import * as ChallengeModal from "./ChallengeModal"; import { fireEvent, render, screen } from "@testing-library/react"; -import { ModalConsumer, ModalProvider, ModalTypes } from "../Modal/ModalProvider"; +import { ModalProvider } from "../Modal/ModalProvider"; +import { ModalContext, ModalTypes } from "../Modal/ModalContext"; import * as DynamicHelp from "react-dynamic-help"; jest.mock("./../Modal", () => { @@ -37,12 +38,12 @@ describe("ChallengeModal", () => { render( - + {({ showModal }) => { showModal(ModalTypes.Challenge); return null; }} - + , ); @@ -71,12 +72,12 @@ describe("ChallengeModal", () => { render( - + {({ showModal }) => { showModal(ModalTypes.Challenge); return null; }} - + , ); diff --git a/src/components/ChallengeModal/ForkModal.tsx b/src/components/ChallengeModal/ForkModal.tsx index cd8ce80614..2d4f9a4342 100644 --- a/src/components/ChallengeModal/ForkModal.tsx +++ b/src/components/ChallengeModal/ForkModal.tsx @@ -23,7 +23,7 @@ import { PlayerAutocomplete } from "@/components/PlayerAutocomplete"; import { MiniGoban } from "@/components/MiniGoban"; import { challenge } from "@/components/ChallengeModal"; import { PlayerCacheEntry } from "@/lib/player_cache"; -import { ModalContext } from "../Modal/ModalProvider"; +import { ModalContext } from "../Modal/ModalContext"; interface ForkModalProperties { goban: GobanRenderer; diff --git a/src/components/LanguagePicker/LanguagePicker.tsx b/src/components/LanguagePicker/LanguagePicker.tsx index cf0473b8b7..19b18407b6 100644 --- a/src/components/LanguagePicker/LanguagePicker.tsx +++ b/src/components/LanguagePicker/LanguagePicker.tsx @@ -18,7 +18,7 @@ import * as React from "react"; import { _, setCurrentLanguage, current_language, languages } from "@/lib/translate"; import * as preferences from "@/lib/preferences"; -import { ModalConsumer, ModalContext, ModalTypes } from "../Modal/ModalProvider"; +import { ModalContext, ModalTypes } from "../Modal/ModalContext"; function language_sorter(a: string, b: string) { if (a === "auto") { @@ -37,17 +37,17 @@ function language_sorter(a: string, b: string) { } export const LanguagePicker = () => ( - - {(value) => ( + + {({ showModal }) => ( value.showModal(ModalTypes.LanguagePicker)} + onClick={() => showModal(ModalTypes.LanguagePicker)} > {languages[current_language]} )} - + ); export const LanguagePickerModal = () => { diff --git a/src/components/Modal/ModalContext.tsx b/src/components/Modal/ModalContext.tsx new file mode 100644 index 0000000000..c244a05225 --- /dev/null +++ b/src/components/Modal/ModalContext.tsx @@ -0,0 +1,36 @@ +/* + * Copyright (C) Online-Go.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { createContext } from "react"; + +export enum ModalTypes { + Challenge = "challenge", + LanguagePicker = "languagePicker", + Fork = "fork", +} + +type ModalContextProps = { + showModal: (type: ModalTypes, props?: any) => void; + hideModal: () => void; +}; + +const defaultModalContext: ModalContextProps = { + showModal: () => {}, + hideModal: () => {}, +}; + +export const ModalContext = createContext(defaultModalContext); diff --git a/src/components/Modal/ModalProvider.tsx b/src/components/Modal/ModalProvider.tsx index 8b92c1b668..b41c99d01c 100644 --- a/src/components/Modal/ModalProvider.tsx +++ b/src/components/Modal/ModalProvider.tsx @@ -18,22 +18,11 @@ /* cspell: words groupadmin cotsen */ import * as React from "react"; -import { ChallengeModal, ChallengeModes } from "../ChallengeModal"; +import { ChallengeModes } from "../ChallengeModal"; import { createPortal } from "react-dom"; -import { LanguagePickerModal } from "../LanguagePicker"; -import { ForkModal } from "../ChallengeModal/ForkModal"; import { GobanRenderer } from "goban"; - -type ModalProviderType = { - showModal: (type: ModalTypes, props?: ModalTypesProps) => void; - hideModal: () => void; -}; - -export enum ModalTypes { - Challenge = "challenge", - LanguagePicker = "languagePicker", - Fork = "fork", -} +import { ModalContext, ModalTypes } from "./ModalContext"; +import { modalRegistry } from "./ModalRegistry"; interface Modals { challenge: { @@ -50,16 +39,11 @@ type ModalTypesProps = { [key: string]: any; }; -export const ModalContext = React.createContext({} as ModalProviderType); -const { Provider, Consumer } = ModalContext; - -export const ModalConsumer = Consumer; - export const ModalProvider = ({ children }: React.PropsWithChildren): JSX.Element => { const [modalType, setModalType] = React.useState(null as ModalTypes | null); const [modalProps, setModalProps] = React.useState({} as ModalTypesProps); - const showModal = (type: ModalTypes, props?: ModalTypesProps) => { + const showModal = (type: ModalTypes, props?: any) => { setModalType(type); switch (type) { @@ -99,24 +83,18 @@ export const ModalProvider = ({ children }: React.PropsWithChildren): JSX.Elemen }, [modalType, hideModal]); return ( - + {modalType && createPortal(
- {modalType === ModalTypes.Challenge && ( - - )} - {modalType === ModalTypes.LanguagePicker && } - {modalType === ModalTypes.Fork && ( - - )} + {React.createElement(modalRegistry[modalType], { + ...modalProps, + onClose: hideModal, + })}
, document.body, )} {children} -
+ ); }; diff --git a/src/components/Modal/ModalRegistry.ts b/src/components/Modal/ModalRegistry.ts new file mode 100644 index 0000000000..70cfb66555 --- /dev/null +++ b/src/components/Modal/ModalRegistry.ts @@ -0,0 +1,38 @@ +/* + * Copyright (C) Online-Go.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { ForkModal } from "../ChallengeModal/ForkModal"; +import { ChallengeModal } from "../ChallengeModal/ChallengeModal"; +import { LanguagePickerModal } from "../LanguagePicker/LanguagePicker"; +import { ModalTypes } from "./ModalContext"; + +interface ModalRegistry { + [key: string]: React.ComponentType; +} + +export const modalRegistry: ModalRegistry = { + [ModalTypes.Fork]: ForkModal, + [ModalTypes.Challenge]: ChallengeModal, + [ModalTypes.LanguagePicker]: LanguagePickerModal, +}; + +export const registerModal = (modalType: string, component: React.ComponentType) => { + modalRegistry[modalType] = component; +}; + +export const unregisterModal = (modalType: string) => { + delete modalRegistry[modalType]; +}; diff --git a/src/views/Game/GameDock.tsx b/src/views/Game/GameDock.tsx index 63d6f88136..3092d9216b 100644 --- a/src/views/Game/GameDock.tsx +++ b/src/views/Game/GameDock.tsx @@ -38,7 +38,7 @@ import { openGameInfoModal } from "./GameInfoModal"; import { useUserIsParticipant } from "./GameHooks"; import { useGoban } from "./goban_context"; import { Tooltip } from "../../components/Tooltip"; -import { ModalConsumer, ModalTypes } from "@/components/Modal/ModalProvider"; +import { ModalContext, ModalTypes } from "@/components/Modal/ModalContext"; interface DockProps { annulled: boolean; @@ -441,7 +441,7 @@ export function GameDock({
- + {({ showModal }) => ( { @@ -468,7 +468,7 @@ export function GameDock({ {_("Fork game")} )} - + diff --git a/src/views/Play/Play.tsx b/src/views/Play/Play.tsx index abaaf9f6b1..4cd0787d40 100644 --- a/src/views/Play/Play.tsx +++ b/src/views/Play/Play.tsx @@ -57,7 +57,7 @@ import { ChallengeFilterKey, shouldDisplayChallenge, } from "@/lib/challenge_utils"; -import { ModalConsumer, ModalTypes } from "../../components/Modal/ModalProvider"; +import { ModalContext, ModalTypes } from "@/components/Modal/ModalContext"; const CHALLENGE_LIST_FREEZE_PERIOD = 1000; // Freeze challenge list for this period while they move their mouse on it @@ -821,7 +821,7 @@ export class Play extends React.Component<{}, PlayState> {
- + {({ showModal }) => { return ( ); }} - +
diff --git a/src/components/LanguagePicker/LanguagePicker.tsx b/src/components/LanguagePicker/LanguagePicker.tsx index 19b18407b6..815d4c3a1f 100644 --- a/src/components/LanguagePicker/LanguagePicker.tsx +++ b/src/components/LanguagePicker/LanguagePicker.tsx @@ -36,19 +36,19 @@ function language_sorter(a: string, b: string) { return 0; } -export const LanguagePicker = () => ( - - {({ showModal }) => ( - showModal(ModalTypes.LanguagePicker)} - > - - {languages[current_language]} - - )} - -); +export const LanguagePicker = () => { + const { showModal } = React.useContext(ModalContext); + + return ( + showModal(ModalTypes.LanguagePicker)} + > + + {languages[current_language]} + + ); +}; export const LanguagePickerModal = () => { const { hideModal } = React.useContext(ModalContext); diff --git a/src/views/Game/GameDock.tsx b/src/views/Game/GameDock.tsx index 3092d9216b..3fcc505c58 100644 --- a/src/views/Game/GameDock.tsx +++ b/src/views/Game/GameDock.tsx @@ -39,6 +39,22 @@ import { useUserIsParticipant } from "./GameHooks"; import { useGoban } from "./goban_context"; import { Tooltip } from "../../components/Tooltip"; import { ModalContext, ModalTypes } from "@/components/Modal/ModalContext"; +import { GobanEngine, GobanRenderer } from "goban"; + +const handleForkGameClick = ( + showModal: (type: ModalTypes, props?: any) => void, + user: rest_api.UserConfig, + engine: GobanEngine, + goban: GobanRenderer, +) => { + if (!user.anonymous && !engine.rengo && !goban.isAnalysisDisabled()) { + if (!goban) { + return; + } + + showModal(ModalTypes.Fork, { goban }); + } +}; interface DockProps { annulled: boolean; @@ -92,6 +108,7 @@ export function GameDock({ const phase = engine.phase; const user = useUser(); + const { showModal } = React.useContext(ModalContext); const tooltipRequired = preferences.get("dock-delay") === MAX_DOCK_DELAY; @@ -441,34 +458,16 @@ export function GameDock({
- - {({ showModal }) => ( - { - if ( - !user.anonymous && - !engine.rengo && - !goban.isAnalysisDisabled() - ) { - if (!goban) { - return; - } - - return showModal(ModalTypes.Fork, { - goban, - }); - } - }} - className={ - user.anonymous || engine.rengo || goban.isAnalysisDisabled() - ? "disabled" - : "" - } - > - {_("Fork game")} - - )} - + handleForkGameClick(showModal, user, engine, goban)} + className={ + user.anonymous || engine.rengo || goban.isAnalysisDisabled() + ? "disabled" + : "" + } + > + {_("Fork game")} + diff --git a/src/views/HelpFlows/ModalHelp.tsx b/src/views/HelpFlows/ModalHelp.tsx index 95c05fd773..91e51c3018 100644 --- a/src/views/HelpFlows/ModalHelp.tsx +++ b/src/views/HelpFlows/ModalHelp.tsx @@ -20,6 +20,7 @@ import * as React from "react"; import { HelpFlow, HelpItem } from "react-dynamic-help"; +import * as DynamicHelp from "react-dynamic-help"; export function ModalHelp(): JSX.Element { return ( @@ -30,3 +31,19 @@ export function ModalHelp(): JSX.Element { ); } + +interface ActivateTooltipProps { + children: React.ReactNode; + flow: string; + item: string; +} + +export const ActivateTooltip = ({ children, flow, item }: ActivateTooltipProps) => ( + + {(value) => { + const { ref: modalHelpIntro } = value.registerTargetItem(`${flow}-${item}`); + value.triggerFlow(flow); + return
{children}
; + }} +
+); diff --git a/src/views/Play/Play.tsx b/src/views/Play/Play.tsx index 4cd0787d40..a2b49c5566 100644 --- a/src/views/Play/Play.tsx +++ b/src/views/Play/Play.tsx @@ -61,6 +61,14 @@ import { ModalContext, ModalTypes } from "@/components/Modal/ModalContext"; const CHALLENGE_LIST_FREEZE_PERIOD = 1000; // Freeze challenge list for this period while they move their mouse on it +const handleComputerChallengeClick = (showModal: (type: ModalTypes, props?: any) => void) => { + if (bot_count() === 0) { + void alert.fire(_("Sorry, all bots seem to be offline, please try again later.")); + return; + } + showModal(ModalTypes.Challenge); +}; + interface PlayState { live_list: Array; correspondence_list: Array; @@ -822,30 +830,18 @@ export class Play extends React.Component<{}, PlayState> {
- {({ showModal }) => { - return ( - - ); - }} + {({ showModal }) => ( + + )}