-
-
- {(corr_automatchers.length || null) && (
-
-
- {_("Your Automatch Requests")}
-
- {this.cellBreaks(7)}
-
- )}
- {(corr_automatchers.length || null) && (
-
- {/* buttons */}
- {_("Rank")}
- {_("Size")}
- {_("Time Control")}
- {_("Handicap")}
- {_("Rules")}
-
- )}
- {corr_automatchers.map((m) => (
-
-
- {
- automatch_manager.cancel(m.uuid);
- if (corr_automatchers.length === 1) {
- this.setState({
- showLoadingSpinnerForCorrespondence: false,
- });
- }
- }}
- >
- {pgettext("Cancel automatch", "Cancel")}
-
-
-
-
- {m.lower_rank_diff === m.upper_rank_diff ? (
- ± {m.lower_rank_diff}
- ) : (
-
- -{m.lower_rank_diff} +{m.upper_rank_diff}
-
- )}
-
-
-
- {m.size_speed_options
- .filter((x) => x.speed === "correspondence")
- .map((x) => x.size)
- .join(",")}
-
-
-
- {m.time_control.condition === "no-preference"
- ? pgettext("Automatch: no preference", "No preference")
- : timeControlSystemText(m.time_control.value.system)}
-
-
-
- {m.handicap.condition === "no-preference"
- ? pgettext("Automatch: no preference", "No preference")
- : m.handicap.value === "enabled"
- ? pgettext("Handicap enabled", "Enabled")
- : pgettext("Handicap disabled", "Disabled")}
-
-
-
- {m.rules.condition === "no-preference"
- ? pgettext("Automatch: no preference", "No preference")
- : rulesText(m.rules.value)}
-
-
-
- ))}
-
-
{_("Custom Games")}
-
-
- {_("Short Games")}
- {this.cellBreaks(8)}
-
-
- {this.anyChallengesToShow(this.state.live_list)
- ? this.challengeListHeaders()
- : null}
-
- {this.challengeList(true)}
-
-
+ }, []);
-
- {_("Long Games")}
- {this.cellBreaks(8)}
-
+ return (
+
+
+
+ {pgettext("Play page", "Matchmaking")}
+
- {this.anyChallengesToShow(this.state.correspondence_list)
- ? this.challengeListHeaders()
- : null}
-
- {this.challengeList(false)}
-
-
-
- {this.state.filter.showRengo && (
-
-
- {_("Rengo")}
-
-
-
- {this.anyChallengesToShow(this.state.rengo_list)
- ? this.rengoListHeaders()
- : null}
-
- {this.rengoList()}
-
-
- )}
-
-
-
- );
- }
-
- automatchContainer() {
- const size_enabled = (size: Size) => {
- return this.state.automatch_size_options.indexOf(size) >= 0;
- };
-
- const own_live_rengo_challenge = this.ownRengoChallengesPending().find((c) =>
- isLiveGame(c.time_control_parameters, c.width, c.height),
- );
- const joined_live_rengo_challenge = this.joinedRengoChallengesPending().find((c) =>
- isLiveGame(c.time_control_parameters, c.width, c.height),
- );
-
- const rengo_challenge_to_show = own_live_rengo_challenge || joined_live_rengo_challenge;
-
- const user = data.get("user");
- const anon = user.anonymous;
- const warned = user.has_active_warning_flag;
-
- // Construction of the pane we need to show...
- if (automatch_manager.active_live_automatcher) {
- return (
-
-
{_("Finding you a game...")}
-
-
-
- {pgettext("Cancel automatch", "Cancel")}
-
-
-
- );
- } else if (this.liveOwnChallengePending()) {
- return (
-
-
{_("Waiting for opponent...")}
-
-
-
- {pgettext("Cancel challenge", "Cancel")}
-
-
-
- );
- } else if (rengo_challenge_to_show) {
- return (
-
-
-
-
- this.setPaneLock(rengo_challenge_to_show.challenge_id, lock)
- }
- />
-
-
- );
- } else if (this.state.showLoadingSpinnerForCorrespondence) {
- return (
-
-
{_("Finding you a game...")}
-
- {_(
- 'This can take several minutes. You will be notified when your match has been found. To view or cancel your automatch requests, please see the list below labeled "Your Automatch Requests".',
- )}
-
-
-
- {_(
- pgettext(
- "Dismiss the 'finding correspondence automatch' message",
- "Got it",
- ),
- )}
-
-
-
- );
- } else {
- return (
-
-
-
{_("Automatch finder")}
-
- this.toggleSize("9x9")}
- >
- 9x9
-
- this.toggleSize("13x13")}
- >
- 13x13
-
- this.toggleSize("19x19")}
- >
- 19x19
-
-
-
-
-
- {_("Settings ")}
-
-
-
-
-
-
this.findMatch("blitz")}
- disabled={anon || warned}
- >
-
- {_("Blitz")}
-
- {pgettext(
- "Automatch average time per move",
- "~10s per move",
- )}
-
-
-
-
this.findMatch("live")}
- disabled={anon || warned}
- >
-
- {_("Normal")}
-
- {pgettext(
- "Automatch average time per move",
- "~30s per move",
- )}
-
-
-
-
-
-
-
- {_("Computer")}
-
-
-
-
this.findMatch("correspondence")}
- disabled={anon || warned}
- >
-
-
- {_("Correspondence")}
-
-
- {pgettext(
- "Automatch average time per move",
- "~1 day per move",
- )}
-
-
-
-
-
-
-
- {_("Create")}
-
-
-
-
- );
- }
- }
-
- suspectChallengeIcon = (C: Challenge): JSX.Element | null =>
- /* Mark eligible suspect games with a warning icon and warning explanation popup.
- We do let users see the warning for their own challenges. */
- (((C.eligible || C.user_challenge) &&
- !C.removed &&
- (C.komi !== null ||
- usedForCheating(C.time_control_parameters) ||
- (C.handicap !== 0 && C.handicap !== -1))) ||
- null) && (
-
-
-
- {(C.komi !== null
- ? pgettext("Warning for users accepting game", "Custom komi") +
- ": " +
- C.komi +
- " "
- : "") +
- (usedForCheating(C.time_control_parameters)
- ? pgettext("Warning for users accepting game", "Unusual time setting") +
- " "
- : "") +
- (C.handicap !== 0 && C.handicap !== -1
- ? pgettext("Warning for users accepting game", "Custom handicap") +
- ": " +
- C.handicap_text
- : "")}
-
-
- );
-
- challengeList(show_live_list: boolean) {
- const challenge_list = show_live_list
- ? this.state.live_list
- : this.state.correspondence_list;
-
- const user = data.get("user");
-
- const timeControlClassName = (config: any) => {
- // This appears to be bolding live games compared to blitz?
- const isBold =
- show_live_list && (config.time_per_move > 3600 || config.time_per_move === 0);
- return "cell " + (isBold ? "bold" : "");
- };
-
- if (!this.anyChallengesToShow(challenge_list)) {
- return (
-
- {
- this.state.filter.showIneligible
- ? _(
- "(none)",
- ) /* translators: There are no challenges in the system, nothing to list here */
- : _(
- "(none available)",
- ) /* translators: There are no challenges that this person is eligible for */
- }
-
- );
- }
-
- return challenge_list.map((C) =>
- shouldDisplayChallenge(C, this.state.filter) ? (
-
-
- {user.is_moderator && (
-
-
-
- )}
-
- {((C.eligible && !C.removed) || null) && (
-
- {_("Accept")}
-
- )}
-
- {(C.user_challenge || null) && (
-
- {_("Remove")}
-
- )}
-
- {this.suspectChallengeIcon(C)}
-
- {(((!C.eligible || C.removed) && !C.user_challenge) || null) && (
-
- {_("Can't accept")}
-
- )}
-
+
setTab("automatch")}
>
-
+ {pgettext("Matchmaking tab (automatch / quick match)", "Quick Match")}
setTab("custom")}
>
- {C.width}x{C.height}
-
-
- {shortShortTimeControl(C.time_control_parameters)}
-
-
{C.ranked_text}
-
{C.handicap_text}
-
{C.komi_text}
-
{C.name}
-
{rulesText(C.rules)}
-
- {(C.user_challenge || null) && }
+ {pgettext("Matchmaking tab", "Custom Games")}
- ) : null,
- );
- }
-
- cellBreaks(amount: number): JSX.Element[] {
- const result: JSX.Element[] = [];
- for (let i = 0; i < amount; ++i) {
- result.push(
);
- }
- return result;
- }
-
- challengeListHeaders() {
- return (
-
- {/* buttons */}
- {_("Player")}
- {/* {_("Rank")} */}
- {_("Size")}
- {_("Time")}
- {_("Ranked")}
- {_("Handicap")}
- {_("Komi")}
-
- {_("Name")}
-
-
- {_("Rules")}
-
- {/* invite link */}
- );
- }
-
- unNominateForRengoChallenge = (C: Challenge) => {
- this.closeChallengeManagementPane(C.challenge_id);
-
- rengo_utils.unNominate(C).catch(errorAlerter);
- };
-
- rengoListHeaders() {
- return (
- <>
-
-
- {/* buttons */ ""}
-
- {_("Organizer")}
- {/* {_("Rank")} */}
- {_("Size")}
- {_("Time")}
- {_("Casual")}
- {_("Auto-Start")}
- {_("Signed up")}
- {_("Handicap")}
- {_("Komi")}
-
- {_("Name")}
-
-
- {/* invite link */ ""}
-
-
- >
- );
- }
- nominateAndShow = (C: Challenge) => {
- this.toggleRengoChallengePane(C.challenge_id);
- rengo_utils.nominateForRengoChallenge(C).catch(errorAlerter);
- };
-
- rengoList = () => {
- if (!this.anyChallengesToShow(this.state.rengo_list)) {
- return (
-
-
-
- {
- this.state.filter.showIneligible
- ? _(
- "(none)",
- ) /* translators: There are no challenges in the system, nothing to list here */
- : _(
- "(none available)",
- ) /* translators: There are no challenges that this person is eligible for */
- }
-
-
-
- );
- }
-
- const user = data.get("user");
-
- const live_list = this.state.rengo_list.filter((c) =>
- isLiveGame(c.time_control_parameters, c.width, c.height),
- );
- const corr_list = this.state.rengo_list.filter(
- (c) => !isLiveGame(c.time_control_parameters, c.width, c.height),
- );
-
- return (
- <>
-
- {_("Live:")}
-
-
-
-
-
-
-
-
-
-
- {_("Correspondence:")}
-
-
- >
- );
- };
-
- rengoChallengeManagementList = (props: { challenge_list: Challenge[]; user: any }) => (
- <>
- {!this.anyChallengesToShow(props.challenge_list) ? (
-
-
- {
- this.state.filter.showIneligible
- ? _(
- "(none)",
- ) /* translators: There are no challenges in the system, nothing to list here */
- : _(
- "(none available)",
- ) /* translators: There are no challenges that this person is eligible for */
- }
-
-
- ) : (
- props.challenge_list.map(
- (C) =>
- (shouldDisplayChallenge(C, this.state.filter) || null) && (
-
-
- {(this.state.show_in_rengo_management_pane.includes(
- C.challenge_id,
- ) ||
- null) && }
-
- ),
- )
- )}
- >
+ {tab === "automatch" &&
}
+ {tab === "custom" &&
}
+
);
-
- openRengoChallengePane = (challenge_id: number) => {
- if (!this.state.show_in_rengo_management_pane.includes(challenge_id)) {
- this.setState({
- show_in_rengo_management_pane: [
- challenge_id,
- ...this.state.show_in_rengo_management_pane,
- ],
- });
- }
- };
-
- toggleRengoChallengePane = (challenge_id: number) => {
- if (this.state.show_in_rengo_management_pane.includes(challenge_id)) {
- this.closeChallengeManagementPane(challenge_id);
- } else {
- this.setState({
- show_in_rengo_management_pane: [
- challenge_id,
- ...this.state.show_in_rengo_management_pane,
- ],
- rengo_manage_pane_lock: {
- ...this.state.rengo_manage_pane_lock,
- [challenge_id]: false,
- },
- });
- }
- };
-
- closeChallengeManagementPane = (challenge_id: number) => {
- if (this.state.show_in_rengo_management_pane.includes(challenge_id)) {
- this.setState({
- show_in_rengo_management_pane: this.state.show_in_rengo_management_pane.filter(
- (c) => c !== challenge_id,
- ),
- });
- }
- };
-
- startOwnRengoChallenge = (challenge: Challenge): Promise
=> {
- // stop the person from pressing "Start" twice impatiently, while we get around to removing this challenge
- this.closeChallengeManagementPane(challenge.challenge_id);
- return rengo_utils.startOwnRengoChallenge(challenge);
- };
-
- rengoManageListItem = (props: { C: Challenge; user: any }) => {
- const { C, user } = { ...props };
- return (
-
-
-
-
-
- this.setPaneLock(C.challenge_id, lock)}
- />
-
-
-
-
- );
- };
-
- rengoListItem = (props: { C: Challenge; user: any }) => {
- const { C, user } = { ...props };
-
- const rengo_casual_mode_text: string = C.rengo_casual_mode ? _("Yes") : _("No");
- const rengo_auto_start_text: number | string = C.rengo_auto_start || "-";
-
- return (
-
-
- {user.is_moderator && (
-
-
-
- )}
-
- {(C.user_challenge || null) && (
-
- {_("Remove")}
-
- )}
-
- {((C.eligible &&
- !C.removed &&
- !C.user_challenge &&
- C.rengo_participants.includes(user.id)) ||
- null) && (
-
- {_("Withdraw")}
-
- )}
-
-
- {C.user_challenge ? _("Manage") : _("View")}
-
-
- {((C.eligible &&
- !C.removed &&
- !C.user_challenge &&
- !C.rengo_participants.includes(user.id)) ||
- null) && (
-
- {_("Join")}
-
- )}
-
- {this.suspectChallengeIcon(C)}
-
- {(((!C.eligible || C.removed) && !C.user_challenge) || null) && (
-
- {_("Can't accept")}
-
- )}
-
-
-
-
-
- {C.width}x{C.height}
-
- {shortShortTimeControl(C.time_control_parameters)}
- {rengo_casual_mode_text}
- {rengo_auto_start_text}
- {C.rengo_participants.length}
- {C.handicap_text}
- {C.komi_text}
- {C.name}
-
- {(C.user_challenge || null) && }
-
-
- );
- };
-}
-
-function challenge_sort(A: Challenge, B: Challenge) {
- if (A.eligible && !B.eligible) {
- return -1;
- }
- if (!A.eligible && B.eligible) {
- return 1;
- }
-
- if (A.user_challenge && !B.user_challenge) {
- return -1;
- }
- if (!A.user_challenge && B.user_challenge) {
- return 1;
- }
-
- const t = A.username.localeCompare(B.username);
- if (t) {
- return t;
- }
-
- if (A.ranked && !B.ranked) {
- return -1;
- }
- if (!A.ranked && B.ranked) {
- return 1;
- }
-
- return A.challenge_id - B.challenge_id;
-}
-
-export function time_per_move_challenge_sort(A: Challenge, B: Challenge) {
- const comparison = Math.sign(A.time_per_move - B.time_per_move);
-
- if (comparison) {
- return comparison;
- }
-
- if (A.eligible && !B.eligible) {
- return -1;
- }
- if (!A.eligible && B.eligible) {
- return 1;
- }
- if (A.user_challenge && !B.user_challenge) {
- return -1;
- }
- if (!A.user_challenge && B.user_challenge) {
- return 1;
- }
-
- const createdA = A.created ? new Date(A.created).getTime() : -Infinity;
- const createdB = B.created ? new Date(B.created).getTime() : -Infinity;
- return createdA - createdB;
}
diff --git a/src/views/Play/PlayContext.ts b/src/views/Play/PlayContext.ts
new file mode 100644
index 0000000000..36ef4a714b
--- /dev/null
+++ b/src/views/Play/PlayContext.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 * as React from "react";
+import { Challenge } from "@/lib/challenge_utils";
+
+export const PlayContext = React.createContext<{
+ closeChallengeManagementPane: (challenge_id: number) => void;
+ cancelOpenRengoChallenge: (challenge: Challenge) => void;
+ unNominateForRengoChallenge: (challenge: Challenge) => void;
+ setPaneLock: (id: number, lock: boolean) => void;
+ toggleRengoChallengePane: (challenge_id: number) => void;
+ cancelOpenChallenge: (challenge: Challenge) => void;
+ unfreezeChallenges: () => void;
+}>(undefined as any);
diff --git a/src/views/Play/QuickMatch.styl b/src/views/Play/QuickMatch.styl
new file mode 100644
index 0000000000..e582206a6b
--- /dev/null
+++ b/src/views/Play/QuickMatch.styl
@@ -0,0 +1,591 @@
+/*
+ * 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 .
+ */
+
+.automatch-container {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+
+ .small-spinner {
+ width: 20px;
+ height: 20px;
+ position: relative;
+ }
+}
+
+.automatch-header {
+ flex-shrink: 0;
+ flex-grow: 0;
+ flex-basis: 2.5rem;
+ font-size: 1.3rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .btn-group {
+ display: flex;
+ flex-wrap: nowrap;
+ }
+}
+
+
+#FindGame {
+ margin-top: 1rem;
+ margin-right: 2rem;
+ margin-left: 2rem;
+ display: grid;
+ // 2x2 grid
+ grid-template-columns: 1fr 1fr;
+ user-select: none;
+ column-gap: 4rem;
+
+ .GameOption-cell {
+ display: flex;
+ flex-direction: column;
+ align-content: stretch;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .automatch-row-container {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ }
+
+
+ .toggle-container {
+ display: flex;
+ align-items: center;
+ }
+ .toggle-label {
+ cursor: pointer;
+ font-size: 0.9rem;
+ themed color shade3
+ margin-right: 0.5rem;
+ }
+
+ .finding-game-container {
+ //flex-shrink: 0;
+ //flex-grow: 0;
+ //flex-basis: 2.5rem;
+ font-size: 1.3rem;
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: 2.5rem;
+
+
+
+ .spinner {
+ margin: 0;
+ height: 20px;
+ width: 20px;
+ margin-left: 1rem;
+ margin-right: 1rem;
+ }
+ button {
+ margin-left: 2rem;
+ font-size: 1.1rem;
+ }
+ }
+
+ .automatch-row {
+ display: flex;
+ justify-content: space-around;
+ align-content: stretch
+ width: 100%;
+ flex-wrap: wrap;
+
+ button {
+ font-size: 1.1rem;
+ font-weight: bold;
+ flex-grow: 1;
+ flex-basis: 13rem;
+ max-width: 13rem;
+ height: 4rem;
+ margin: 0.5rem;
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+ align-content: center;
+ justify-content: center;
+
+ .play-button-text-root {
+ position: relative;
+ height: 1.5rem;
+
+
+ .time-per-move {
+ position: absolute;
+ bottom: -1.2rem;
+ left: 0;
+ right: 0;
+ text-align: center;
+ }
+ }
+
+ .fa, .ogs-turtle {
+ margin: 0.2rem;
+ }
+
+ .ogs-turtle {
+ position: relative;
+ width: 1.0rem;
+ }
+ .ogs-turtle::before {
+ position: absolute;
+ top: -0.5rem;
+ left: 0;
+ }
+
+ .time-per-move {
+ font-size: font-size-smaller;
+ }
+ }
+ }
+
+
+ .automatch-settings {
+ font-size: 1rem;
+ flex: 0;
+ text-align: left;
+ white-space: nowrap;
+
+ .automatch-settings-link {
+ cursor: pointer;
+ }
+
+ i {
+ padding-right: 0.25rem;
+ }
+ }
+
+ .automatch-settings-corr {
+ flex: 0;
+ text-align: justify;
+ font-size: 1rem;
+ padding: .5rem;
+ padding-left: 3rem;
+ }
+
+ .custom-game-row {
+ display: flex;
+ justify-content: space-around;
+ align-content: stretch
+ width: 100%;
+
+ button {
+ font-size: 1.1rem;
+ font-weight: bold;
+ flex-grow: 1;
+ width: 100%;
+ max-width: 13rem;
+ margin: 0.5rem;
+ height: 4rem;
+
+ .fa {
+ margin: 0.2rem;
+ }
+ }
+ }
+
+ .custom-game-header {
+ display: flex;
+ justify-content: space-around;
+ align-content: stretch
+ width: 100%;
+ justify-content: space-between;
+ align-items: left;
+ font-size: 1.3rem;
+ margin-top: 1rem;
+ }
+
+
+ .GameOption {
+ margin-bottom: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-content: stretch
+ align-items: center;
+ width: 100%;
+ flex-wrap: wrap;
+ font-weight: bold;
+ font-size: 1.3rem;
+
+ .dropdown {
+ display: inline-flex;
+ justify-content: space-between;
+ min-width: 15rem;
+ align-items: center;
+ themed background-color shade5
+ border-radius: 0.2rem;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ cursor: pointer;
+
+ &:hover {
+ themed background-color shade4
+ }
+
+ .DownCarret {
+ display: inline-block;
+ float: right;
+ padding-left: 1.5rem;
+ padding-right: -0.5rem;
+ margin: 0.5rem;
+ }
+ }
+
+
+
+ }
+ .MiniGoban {
+ width: 22rem !important;
+ height: 22rem !important;
+ }
+ .subtext {
+ font-size: 0.9rem;
+ }
+
+ .play-button {
+ //margin-top: 3rem;
+ width: 100%;
+ height: 6rem;
+ font-size: 1.5rem;
+ line-height: 1.5rem;
+ text-align: center;
+ margin-bottom: 0;
+
+ }
+
+ .game-speed-option-container {
+ //margin-top: 1rem;
+ margin-bottom: 2rem;
+ border-radius: 0.5rem;
+ cursor: pointer;
+ }
+ .game-speed-option {
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+ flex-wrap: wrap;
+
+ .game-speed-button {
+ border-width: 0.15rem !important;
+ font-size: 1.3rem;
+
+ }
+ &.active .game-speed-button {
+ themed-important border-color primary
+ }
+
+
+ .game-speed-option-toggle {
+ display: inline-flex;
+ align-items: center;
+
+ .TogglePreferRequired {
+ margin-left: 1rem;
+ margin-right: 1rem;
+
+ }
+ }
+
+ .time-control-button {
+ border-width: 0.15rem !important;
+ text-align: center;
+ height: 3rem;
+ min-width: 8rem;
+ }
+
+ &.active {
+ .prefer .time-control-button,
+ .indifferent .time-control-button,
+ .require .time-control-button {
+ themed-important border-color primary
+ }
+ }
+ .exclude .time-control-button {
+ //themed-important border-color danger
+ themed-desaturate background-color default-button button-disabled-desaturate-amount
+ themed color shade2
+ themed-desaturate border-color default-button button-disabled-desaturate-amount
+ box-shadow: none;
+ }
+
+
+ .time-control {
+ font-weight: bold;
+ }
+
+
+ }
+
+ .game-speed-title {
+ font-size: 1.2rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+
+ .description {
+ //padding-left: 1rem;
+ //padding-right: 1rem;
+ font-style: italic;
+ font-size: 0.9rem;
+ }
+ }
+
+ .game-speed-buttons {
+ //padding-bottom: 0.5rem;
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ //border-bottom: 0.15rem solid transparent;
+
+ &.flexible-active {
+ //themed-important border-color shade3
+ }
+
+ .time-control-button {
+ min-width: 10rem;
+ }
+
+ .or {
+ display: inline-block;
+ min-width: 2rem;
+ text-align: center;
+ }
+ }
+
+ .speed-options {
+ margin-bottom: 2rem;
+ }
+
+ .opponent-options {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: stretch;
+ align-items: stretch;
+ align-content: stretch;
+ flex-grow: 1;
+
+ margin-top: 1rem;
+
+ .opponent-option-container {
+ display: inline-flex;
+ flex-direction: column;
+ flex-grow: 1;
+ height: 6rem;
+ margin-left: 0.5rem;
+ margin-right: 0.5rem;
+ transition: background-color 0.3s ease;
+ box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.15);
+ border: 1px solid transparent;
+ border-radius: 0.3rem;
+ padding-left: 0.6em;
+ padding-right: 0.6em;
+ //padding-top: 0.6em;
+ //padding-bottom: 0.6em;
+ min-width: 12rem;
+ cursor: pointer;
+ themed background-color default-button
+ themed color default-button-color
+
+ &:hover, &:focus {
+ text-decoration: none;
+
+ themed-lighten background-color default-button button-highlight-brighten-amount
+ themed-darken border-color default-button button-border-highlight-darken-amount
+ }
+ &:active, &.active {
+ text-decoration: none;
+
+ themed-darken background-color default-button button-highlight-brighten-amount
+ themed-important border-color primary
+ }
+
+ &.disabled {
+ themed-desaturate background-color default-button button-disabled-desaturate-amount
+ themed color shade2
+ themed-desaturate border-color default-button button-disabled-desaturate-amount
+ box-shadow: none;
+ cursor: default;
+ cursor: not-allowed;
+
+ select {
+ themed color shade2
+ }
+ }
+
+ .opponent-title {
+ text-align: center;
+ font-size: 1.3rem;
+ padding-bottom: 0.5rem;
+ padding-top: 0.5rem;
+ }
+
+ .opponent-rank-range {
+ display: flex;
+ justify-content: space-around;
+ }
+
+ .computer-select {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+
+
+ select {
+ flex-grow: 1;
+ min-width: 10rem;
+ }
+
+ .fa {
+ padding-left: 1rem;
+ }
+ }
+ }
+ }
+
+ .anonymous-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ margin-top: 2rem;
+ margin-bottom: 2rem;
+ font-size: 1.3rem;
+ .Link {
+ margin-left: 0.5rem;
+ margin-right: 0.5rem;
+ }
+ }
+
+ .ogs-react-select__control {
+ //min-height: 2rem;
+ //height: 2rem;
+ themed color shade1
+ }
+ .ogs-react-select__control, .ogs-react-select__menu {
+ font-size: 1.0rem;
+ font-weight: normal;
+ }
+
+ .ogs-react-select__menu {
+ right: 0;
+ width: 20rem;
+ max-width: 90vw;
+
+ .ogs-react-select__group-heading {
+ text-transform: none;
+ font-size: 0.9rem;
+ }
+
+ .option {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+
+ &.focused {
+ themed-important background-color shade3
+ }
+ &.selected {
+ themed-important background-color shade3
+ }
+ themed color fg
+ }
+
+ .option-label {
+ font-weight: bold;
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+ .option-description {
+ font-size: 0.9rem;
+ font-style: italic;
+ themed color shade1
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+ }
+
+}
+
+@media (max-width: hamburger-cutoff) {
+ #FindGame {
+ display: flex;
+ flex-direction: column;
+ margin-right: 0rem;
+ margin-left: 0rem;
+ margin-bottom: 4rem;
+ column-gap: 0rem;
+ }
+
+ .GameOption-cell {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ justify-content: space-between;
+ margin-top: 1rem;
+ }
+
+ .tab-head {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+
+ .tabs-container {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ margin-bottom: 1rem;
+ }
+ }
+
+ .game-speed-buttons {
+ padding-left: 1rem !important;
+ padding-right: 1rem !important;
+
+ button.time-control-button {
+ min-width: 8rem !important;
+ }
+ }
+
+ .opponent-option-container {
+ margin-bottom: 2rem;
+ }
+
+ .Play {
+ .Card {
+ margin: 0;
+ }
+ .game-speed-option {
+ .time-control-button {
+ min-width: 4rem !important;
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/views/Play/QuickMatch.tsx b/src/views/Play/QuickMatch.tsx
new file mode 100644
index 0000000000..ae3ea1fbc9
--- /dev/null
+++ b/src/views/Play/QuickMatch.tsx
@@ -0,0 +1,948 @@
+/*
+ * 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 * as React from "react";
+import * as data from "@/lib/data";
+import * as preferences from "@/lib/preferences";
+import moment from "moment";
+
+import {
+ AutomatchPreferences,
+ JGOFTimeControlSpeed,
+ shortDurationString,
+ Size,
+ Speed,
+} from "goban";
+import { _, pgettext } from "@/lib/translate";
+import { automatch_manager } from "@/lib/automatch_manager";
+import { bot_event_emitter, bots_list } from "@/lib/bots";
+import { alert } from "@/lib/swal_config";
+import { useRefresh, useUser } from "@/lib/hooks";
+//import { Toggle } from "@/components/Toggle";
+import { MiniGoban } from "@/components/MiniGoban";
+import { getUserRating, rankString } from "@/lib/rank_utils";
+import { errorAlerter, uuid } from "@/lib/misc";
+import { LoadingButton } from "@/components/LoadingButton";
+import { post } from "@/lib/requests";
+import { browserHistory } from "@/lib/ogsHistory";
+import {
+ ChallengeDetails,
+ RejectionDetails,
+ rejectionDetailsToMessage,
+} from "@/components/ChallengeModal";
+import { notification_manager, NotificationManagerEvents } from "@/components/Notifications";
+import { socket } from "@/lib/sockets";
+import { sfx } from "@/lib/sfx";
+import { Link } from "react-router-dom";
+import Select from "react-select";
+
+moment.relativeTimeThreshold("m", 56);
+export interface SelectOption {
+ break?: JSX.Element;
+ value: string;
+ label: string;
+}
+
+interface OptionWithDescription {
+ value: string;
+ label: string;
+ description: string;
+}
+
+const game_clock_options: OptionWithDescription[] = [
+ {
+ value: "exact",
+ label: _("Exact"),
+ description: pgettext("Game Clock option description for Exact", "Pick one time setting"),
+ },
+ {
+ value: "flexible",
+ label: _("Flexible"),
+ description: pgettext(
+ "Game Clock option description for Flexible",
+ "Prefer one time setting, but accept the other similarly paced time setting",
+ ),
+ },
+];
+
+const handicap_options: OptionWithDescription[] = [
+ {
+ value: "enabled",
+ label: pgettext(
+ "Matchmaking handicap option: require handicaps for games between players with different ranks",
+ "Required",
+ ),
+ description: _("Require handicaps between players with different ranks"),
+ },
+ {
+ value: "standard",
+ label: pgettext(
+ "Matchmaking handicap option: standard, prefer handicaps but allow even games",
+ "Standard",
+ ),
+ description: _("Use handicaps by default, but accept games with handicaps disabled"),
+ },
+ {
+ value: "disable",
+ label: pgettext("Matchmaking handicap option: disable handicaps", "Disabled"),
+ description: _("Disable handicaps"),
+ },
+];
+
+const RenderOptionWithDescription = (props: {
+ data: OptionWithDescription;
+ innerProps: any;
+ innerRef: any;
+ isFocused: boolean;
+ isSelected: boolean;
+}) => {
+ const opt = props.data;
+ console.log(props);
+ return (
+
+
{opt.label}
+
{opt.description}
+
+ );
+};
+
+const select_styles = {
+ menu: ({ ...css }) => ({
+ ...css,
+ width: "20rem",
+ }),
+};
+
+interface GameSpeedOptions {
+ [size: string]: {
+ [speed: string]: {
+ time_estimate: string;
+ fischer: {
+ initial_time: number;
+ time_increment: number;
+ time_estimate: string;
+ };
+ byoyomi?: {
+ main_time: number;
+ periods: number;
+ period_time: number;
+ time_estimate: string;
+ };
+ };
+ };
+}
+
+const SPEED_OPTIONS: GameSpeedOptions = {
+ "9x9": {
+ blitz: {
+ //time_estimate: "\u223c 4\u2212" + moment.duration(6, "minutes").humanize(),
+ time_estimate: "\u223c " + moment.duration(5, "minutes").humanize(),
+ fischer: {
+ initial_time: 30,
+ time_increment: 3,
+ //time_estimate: "~ 4-" + moment.duration(6, "minutes").humanize(),
+ time_estimate: "\u223c 4\u2212" + moment.duration(6, "minutes").humanize(),
+ },
+ byoyomi: {
+ main_time: 30,
+ periods: 5,
+ period_time: 10,
+ time_estimate: "\u223c 4\u2212" + moment.duration(6, "minutes").humanize(),
+ },
+ },
+ rapid: {
+ //time_estimate: "\u223c 7\u2212" + moment.duration(14, "minutes").humanize(),
+ time_estimate: "\u223c " + moment.duration(10, "minutes").humanize(),
+ fischer: {
+ initial_time: 120,
+ time_increment: 5,
+ time_estimate: "\u223c 7\u2212" + moment.duration(9, "minutes").humanize(),
+ },
+ byoyomi: {
+ main_time: 120,
+ periods: 5,
+ period_time: 30,
+ time_estimate: "\u223c 8\u2212" + moment.duration(14, "minutes").humanize(),
+ },
+ },
+ live: {
+ //time_estimate: "\u223c 9\u2212" + moment.duration(17, "minutes").humanize(),
+ time_estimate: "\u223c " + moment.duration(15, "minutes").humanize(),
+ fischer: {
+ initial_time: 180,
+ time_increment: 10,
+ time_estimate: "\u223c 9\u2212" + moment.duration(13, "minutes").humanize(),
+ },
+ byoyomi: {
+ main_time: 300,
+ periods: 5,
+ period_time: 30,
+ time_estimate: "\u223c 11\u2212" + moment.duration(17, "minutes").humanize(),
+ },
+ },
+ correspondence: {
+ time_estimate: pgettext("Game speed: multi-day games", "Daily Correspondence"),
+ fischer: {
+ initial_time: 86400 * 3,
+ time_increment: 86400,
+ time_estimate: "",
+ },
+ },
+ },
+ "13x13": {
+ blitz: {
+ //time_estimate: "\u223c 8\u2212" + moment.duration(10, "minutes").humanize(),
+ time_estimate: "\u223c " + moment.duration(10, "minutes").humanize(),
+ fischer: {
+ initial_time: 30,
+ time_increment: 3,
+ time_estimate: "\u223c 8\u2212" + moment.duration(15, "minutes").humanize(),
+ },
+ byoyomi: {
+ main_time: 30,
+ periods: 5,
+ period_time: 10,
+ time_estimate: "\u223c 11\u2212" + moment.duration(17, "minutes").humanize(),
+ },
+ },
+ rapid: {
+ //time_estimate: "\u223c 16\u2212" + moment.duration(25, "minutes").humanize(),
+ time_estimate: "\u223c " + moment.duration(20, "minutes").humanize(),
+ fischer: {
+ initial_time: 180,
+ time_increment: 5,
+ time_estimate: "\u223c 16\u2212" + moment.duration(20, "minutes").humanize(),
+ },
+ byoyomi: {
+ main_time: 180,
+ periods: 5,
+ period_time: 30,
+ time_estimate: "\u223c 18\u2212" + moment.duration(25, "minutes").humanize(),
+ },
+ },
+ live: {
+ //time_estimate: "\u223c 20\u2212" + moment.duration(35, "minutes").humanize(),
+ time_estimate: "\u223c " + moment.duration(30, "minutes").humanize(),
+ fischer: {
+ initial_time: 300,
+ time_increment: 10,
+ time_estimate: "\u223c 20\u2212" + moment.duration(30, "minutes").humanize(),
+ },
+ byoyomi: {
+ main_time: 600,
+ periods: 5,
+ period_time: 30,
+ time_estimate: "\u223c 20\u2212" + moment.duration(35, "minutes").humanize(),
+ },
+ },
+ correspondence: {
+ time_estimate: pgettext("Game speed: multi-day games", "Daily Correspondence"),
+ fischer: {
+ initial_time: 86400 * 3,
+ time_increment: 86400,
+ time_estimate: "",
+ },
+ },
+ },
+ "19x19": {
+ blitz: {
+ //time_estimate: "\u223c 10\u2212" + moment.duration(15, "minutes").humanize(),
+ time_estimate: "\u223c " + moment.duration(15, "minutes").humanize(),
+ fischer: {
+ initial_time: 30,
+ time_increment: 3,
+ time_estimate: "\u223c 10\u2212" + moment.duration(15, "minutes").humanize(),
+ },
+ byoyomi: {
+ main_time: 30,
+ periods: 5,
+ period_time: 10,
+ time_estimate: "\u223c 11\u2212" + moment.duration(17, "minutes").humanize(),
+ },
+ },
+ rapid: {
+ //time_estimate: "\u223c 21\u2212" + moment.duration(31, "minutes").humanize(),
+ time_estimate: "\u223c " + moment.duration(25, "minutes").humanize(),
+ fischer: {
+ initial_time: 300,
+ time_increment: 5,
+ time_estimate: "\u223c 21\u2212" + moment.duration(31, "minutes").humanize(),
+ },
+ byoyomi: {
+ main_time: 300,
+ periods: 5,
+ period_time: 30,
+ time_estimate: "\u223c 20\u2212" + moment.duration(35, "minutes").humanize(),
+ },
+ },
+ live: {
+ //time_estimate: "\u223c 26\u2212" + moment.duration(52, "minutes").humanize(),
+ time_estimate: "\u223c " + moment.duration(40, "minutes").humanize(),
+ fischer: {
+ initial_time: 600,
+ time_increment: 10,
+ time_estimate: "\u223c 26\u2212" + moment.duration(52, "minutes").humanize(),
+ },
+ byoyomi: {
+ main_time: 1200,
+ periods: 5,
+ period_time: 30,
+ time_estimate: "\u223c 28\u2212" + moment.duration(49, "minutes").humanize(),
+ },
+ },
+ correspondence: {
+ time_estimate: pgettext("Game speed: multi-day games", "Daily Correspondence"),
+ fischer: {
+ initial_time: 86400 * 3,
+ time_increment: 86400,
+ time_estimate: "",
+ },
+ },
+ },
+};
+
+export function QuickMatch(): JSX.Element {
+ const user = useUser();
+ const refresh = useRefresh();
+ const [board_size, setBoardSize] = preferences.usePreference("automatch.size");
+ const [game_speed, setGameSpeed] = preferences.usePreference("automatch.speed");
+ const [handicaps, setHandicaps] = preferences.usePreference("automatch.handicaps");
+ const [time_control_system, setTimeControlSystem] =
+ preferences.usePreference("automatch.time-control");
+ const [opponent, setOpponent] = preferences.usePreference("automatch.opponent");
+ const [selected_bot, setSelectedBot] = preferences.usePreference("automatch.bot");
+ const [lower_rank_diff, setLowerRankDiff] = preferences.usePreference(
+ "automatch.lower-rank-diff",
+ );
+ const [upper_rank_diff, setUpperRankDiff] = preferences.usePreference(
+ "automatch.upper-rank-diff",
+ );
+ const bot_select = React.useRef(null);
+
+ const [correspondence_spinner, setCorrespondenceSpinner] = React.useState(false);
+ const [bot_spinner, setBotSpinner] = React.useState(false);
+ const cancel_bot_game = React.useRef<() => void>(() => {});
+ const [game_clock, setGameClock] = preferences.usePreference("automatch.game-clock");
+
+ React.useEffect(() => {
+ automatch_manager.on("entry", refresh);
+ automatch_manager.on("start", refresh);
+ automatch_manager.on("cancel", refresh);
+ bot_event_emitter.on("updated", refresh);
+
+ return () => {
+ automatch_manager.off("entry", refresh);
+ automatch_manager.off("start", refresh);
+ automatch_manager.off("cancel", refresh);
+ bot_event_emitter.off("updated", refresh);
+ };
+ }, []);
+
+ const anon = user.anonymous;
+ const warned = user.has_active_warning_flag;
+
+ const cancelActiveAutomatch = React.useCallback(() => {
+ if (automatch_manager.active_live_automatcher) {
+ automatch_manager.cancel(automatch_manager.active_live_automatcher.uuid);
+ }
+ refresh();
+ }, [refresh]);
+
+ const doAutomatch = React.useCallback(() => {
+ if (data.get("user").anonymous) {
+ void alert.fire(_("Please sign in first"));
+ return;
+ }
+
+ // Open challenge
+ console.log("findMatch", board_size, game_speed);
+
+ const size_speed_options: Array<{ size: Size; speed: Speed }> = [
+ { size: board_size, speed: game_speed },
+ ];
+
+ const preferences: AutomatchPreferences = {
+ uuid: uuid(),
+ size_speed_options,
+ lower_rank_diff,
+ upper_rank_diff,
+ rules: {
+ condition: "required",
+ value: "japanese",
+ },
+ time_control: {
+ condition: game_clock === "flexible" ? "preferred" : "required",
+ value: {
+ system: time_control_system,
+ },
+ },
+ handicap: {
+ condition: handicaps === "standard" ? "preferred" : "required",
+ value: handicaps === "disabled" ? "disabled" : "enabled",
+ },
+ };
+ console.log(preferences);
+
+ automatch_manager.findMatch(preferences);
+ refresh();
+
+ if (game_speed === "correspondence") {
+ setCorrespondenceSpinner(true);
+ }
+ }, [
+ board_size,
+ game_speed,
+ opponent,
+ lower_rank_diff,
+ upper_rank_diff,
+ handicaps,
+ game_clock,
+ time_control_system,
+ refresh,
+ automatch_manager,
+ setCorrespondenceSpinner,
+ ]);
+
+ const playComputer = React.useCallback(() => {
+ const challenge: ChallengeDetails = {
+ initialized: false,
+ min_ranking: -99,
+ max_ranking: 99,
+ challenger_color: "automatic",
+ rengo_auto_start: 0,
+ game: {
+ name: _("Quick Match"),
+ rules: "chinese",
+ ranked: true,
+ width: board_size === "9x9" ? 9 : board_size === "13x13" ? 13 : 19,
+ height: board_size === "9x9" ? 9 : board_size === "13x13" ? 13 : 19,
+ handicap: handicaps === "disabled" ? 0 : -1,
+ komi_auto: "automatic",
+ komi: 0,
+ disable_analysis: false,
+ initial_state: null,
+ private: false,
+ rengo: false,
+ rengo_casual_mode: false,
+ pause_on_weekends: true,
+ time_control: time_control_system,
+ time_control_parameters:
+ time_control_system === "fischer"
+ ? {
+ system: "fischer",
+ speed: game_speed,
+ initial_time:
+ SPEED_OPTIONS[board_size as any][game_speed].fischer.initial_time,
+ time_increment:
+ SPEED_OPTIONS[board_size as any][game_speed].fischer
+ .time_increment,
+ max_time:
+ SPEED_OPTIONS[board_size as any][game_speed].fischer
+ .initial_time * 10,
+ pause_on_weekends: true,
+ }
+ : {
+ system: "byoyomi",
+ speed: game_speed,
+ main_time:
+ SPEED_OPTIONS[board_size as any][game_speed].byoyomi!.main_time,
+ periods:
+ SPEED_OPTIONS[board_size as any][game_speed].byoyomi!.periods,
+ period_time:
+ SPEED_OPTIONS[board_size as any][game_speed].byoyomi!.period_time,
+ periods_min:
+ SPEED_OPTIONS[board_size as any][game_speed].byoyomi!.periods,
+ periods_max:
+ SPEED_OPTIONS[board_size as any][game_speed].byoyomi!.periods,
+ pause_on_weekends: true,
+ },
+ },
+ };
+
+ const bot_id = bot_select.current?.value || selected_bot;
+ if (!bot_id) {
+ void alert.fire(_("Please select a bot"));
+ return;
+ }
+
+ setBotSpinner(true);
+ post(`players/${bot_id}/challenge`, challenge)
+ .then((res) => {
+ const challenge_id = res.challenge;
+
+ const game_id = typeof res.game === "object" ? res.game.id : res.game;
+ let keepalive_interval: ReturnType | undefined;
+
+ const checkForReject = (
+ notification: NotificationManagerEvents["notification"],
+ ) => {
+ console.log("challenge rejection check notification:", notification);
+ if (notification.type === "gameOfferRejected") {
+ /* non checked delete to purge old notifications that
+ * could be around after browser refreshes, connection
+ * drops, etc. */
+ notification_manager.deleteNotification(notification);
+ if (notification.game_id === game_id) {
+ onRejected(notification.message, notification.rejection_details);
+ }
+ }
+ };
+
+ const active_check = () => {
+ keepalive_interval = setInterval(() => {
+ socket.send("challenge/keepalive", {
+ challenge_id: challenge_id,
+ game_id: game_id,
+ });
+ }, 1000);
+ socket.send("game/connect", { game_id: game_id });
+ socket.on(`game/${game_id}/gamedata`, onGamedata);
+ };
+
+ const onGamedata = () => {
+ off();
+ alert.close();
+ //sfx.play("game_accepted");
+ sfx.play("game_started", 3000);
+ //sfx.play("setup-bowl");
+ browserHistory.push(`/game/${game_id}`);
+ };
+
+ const onRejected = (message?: string, details?: RejectionDetails) => {
+ off();
+ alert.close();
+ void alert.fire({
+ text:
+ (details && rejectionDetailsToMessage(details)) ||
+ message ||
+ _("Game offer was rejected"),
+ });
+ };
+
+ const off = () => {
+ clearTimeout(keepalive_interval);
+ socket.send("game/disconnect", { game_id: game_id });
+ socket.off(`game/${game_id}/gamedata`, onGamedata);
+ //socket.off(`game/${game_id}/rejected`, onRejected);
+ notification_manager.event_emitter.off("notification", checkForReject);
+ cancel_bot_game.current = () => {};
+ setBotSpinner(false);
+ };
+
+ cancel_bot_game.current = off;
+
+ notification_manager.event_emitter.on("notification", checkForReject);
+ active_check();
+ })
+ .catch((err) => {
+ setBotSpinner(false);
+ errorAlerter(err);
+ });
+ }, [selected_bot, board_size, handicaps, game_speed, time_control_system, refresh]);
+
+ const play = React.useCallback(() => {
+ if (data.get("user").anonymous) {
+ void alert.fire(_("Please sign in first"));
+ return;
+ }
+
+ if (opponent === "bot") {
+ playComputer();
+ } else {
+ doAutomatch();
+ }
+ }, [doAutomatch, playComputer]);
+
+ const dismissCorrespondenceSpinner = React.useCallback(() => {
+ setCorrespondenceSpinner(false);
+ }, []);
+
+ /*
+ const newComputerGame = React.useCallback(() => {
+ if (bot_count() === 0) {
+ void alert.fire(_("Sorry, all bots seem to be offline, please try again later."));
+ return;
+ }
+ challengeComputer();
+ }, []);
+ */
+
+ const search_active =
+ !!automatch_manager.active_live_automatcher || correspondence_spinner || bot_spinner;
+
+ // Construction of the pane we need to show...
+ return (
+
+ {/* Board Size */}
+
+
+ {_("Board Size")}
+
+
+
+ {(["9x9", "13x13", "19x19"] as Size[]).map((s) => (
+ {
+ setBoardSize(s);
+ }}
+ >
+ {s}
+
+ ))}
+
+
+
+
+
+ {/* Game Speed */}
+
+
+ {pgettext("Clock settings header for a new game", "Game Clock")}
+ o.value === game_clock)}
+ onChange={(opt) => {
+ if (opt) {
+ setGameClock(opt.value as "flexible" | "exact");
+ }
+ }}
+ options={game_clock_options}
+ components={{ Option: RenderOptionWithDescription }}
+ />
+
+
+
+ {(["blitz", "rapid", "live", "correspondence"] as JGOFTimeControlSpeed[]).map(
+ (speed) => {
+ const opt = SPEED_OPTIONS[board_size as any][speed];
+
+ return (
+
setGameSpeed(speed)}
+ key={speed}
+ >
+
+ {opt.time_estimate}
+ {/*
+
+ {opt.fischer.time_estimate || opt.time_estimate}
+
+ {opt.byoyomi?.time_estimate && (
+
+ {opt.byoyomi.time_estimate}
+
+ )}
+ */}
+
+
+ {
+ setGameSpeed(speed);
+ setTimeControlSystem("fischer");
+ }}
+ >
+ {shortDurationString(opt.fischer.initial_time)}
+ {" + "}
+ {shortDurationString(opt.fischer.time_increment)}
+
+ {opt.byoyomi && (
+ <>
+ {game_clock === "flexible" &&
+ game_speed === speed ? (
+
+ {pgettext(
+ "Used on the play page to indicate that either time control preference may be used (5m+5s _or_ 5m+5x30s)",
+ "or",
+ )}
+
+ ) : (
+
+ )}
+ {
+ setGameSpeed(speed);
+ setTimeControlSystem("byoyomi");
+ }}
+ disabled={search_active}
+ >
+ {shortDurationString(opt.byoyomi.main_time)}
+ {" + "}
+ {opt.byoyomi.periods}x
+ {shortDurationString(
+ opt.byoyomi.period_time,
+ ).trim()}
+
+ >
+ )}
+
+
+ );
+ },
+ )}
+
+
+
+ {/* Opponent */}
+
+
+ {_("Opponent")}
+
+
+
+
{
+ if (search_active) {
+ return;
+ }
+ setOpponent("human");
+ }}
+ >
+
+ {pgettext("Play a human opponent", "Human")}
+
+
+ setLowerRankDiff(parseInt(ev.target.value))}
+ disabled={search_active}
+ >
+ {user.anonymous ? (
+ {"30k"}
+ ) : (
+ [9, 8, 7, 6, 5, 4, 3, 2, 1, 0].map((v) => (
+
+ {rankString(user.ranking - v)}
+
+ ))
+ )}
+
+ {" - "}
+ setUpperRankDiff(parseInt(ev.target.value))}
+ disabled={search_active}
+ >
+ {user.anonymous ? (
+ {"9d"}
+ ) : (
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((v) => (
+
+ {rankString(user.ranking + v)}
+
+ ))
+ )}
+
+
+
+
{
+ if (search_active) {
+ return;
+ }
+ setOpponent("bot");
+ }}
+ >
+
+ {pgettext("Play a computer opponent", "Computer")}
+
+
+
setSelectedBot(parseInt(ev.target.value))}
+ required={true}
+ disabled={search_active}
+ >
+ {bots_list().map((bot, idx) => (
+
+ {bot.username} ({rankString(getUserRating(bot).rank)})
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ {/* Play Button */}
+
+
+ {_("Handicap")}
+ o.value === handicaps)}
+ isSearchable={false}
+ minMenuHeight={400}
+ maxMenuHeight={400}
+ menuPlacement="auto"
+ onChange={(opt) => {
+ if (opt) {
+ setHandicaps(opt.value as "enabled" | "standard" | "disabled");
+ }
+ }}
+ options={[
+ {
+ label: _(
+ "Handicaps balance games between players of different ranks by adjusting starting stones and komi points.",
+ ),
+ options: handicap_options,
+ },
+ ]}
+ components={{
+ Option: RenderOptionWithDescription,
+ }}
+ />
+
+
+ {automatch_manager.active_live_automatcher && (
+
+
+ {_("Finding you a game...")}
+
+
+ {pgettext("Cancel automatch", "Cancel search")}
+
+
+
+ )}
+
+ {bot_spinner && (
+
+
+
+ {_("Cancel")}
+
+
+
+ )}
+
+ {correspondence_spinner && (
+
+
{_("Finding you a game...")}
+
+ {_(
+ 'This can take several minutes. You will be notified when your match has been found. To view or cancel your automatch requests, please see the list below labeled "Your Automatch Requests".',
+ )}
+
+
+
+ {_(
+ pgettext(
+ "Dismiss the 'finding correspondence automatch' message",
+ "Got it",
+ ),
+ )}
+
+
+
+ )}
+ {user.anonymous && (
+
+ {_("Please sign in to play")}
+
+ {_("Register for Free")}
+ {" | "}
+ {_("Sign in")}
+
+
+ )}
+
+ {!search_active && !user.anonymous && (
+
+ {_("Play")}
+
+ )}
+
+
+ );
+}
diff --git a/src/views/Play/index.ts b/src/views/Play/index.ts
index 000140d385..80e5233ae2 100644
--- a/src/views/Play/index.ts
+++ b/src/views/Play/index.ts
@@ -16,3 +16,4 @@
*/
export * from "./Play";
+export * from "./ChallengeLists";
diff --git a/src/views/Play/utils.ts b/src/views/Play/utils.ts
new file mode 100644
index 0000000000..d029abc6d9
--- /dev/null
+++ b/src/views/Play/utils.ts
@@ -0,0 +1,80 @@
+/*
+ * 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 { ChallengeFilter, Challenge } from "@/lib/challenge_utils";
+
+export function anyChallengesToShow(filter: ChallengeFilter, challenge_list: Challenge[]): boolean {
+ return (
+ (filter.showIneligible && (challenge_list.length as any)) ||
+ challenge_list.reduce((accumulator, current) => {
+ return accumulator || current.eligible || !!current.user_challenge;
+ }, false)
+ );
+}
+export function challenge_sort(A: Challenge, B: Challenge) {
+ if (A.eligible && !B.eligible) {
+ return -1;
+ }
+ if (!A.eligible && B.eligible) {
+ return 1;
+ }
+
+ if (A.user_challenge && !B.user_challenge) {
+ return -1;
+ }
+ if (!A.user_challenge && B.user_challenge) {
+ return 1;
+ }
+
+ const t = A.username.localeCompare(B.username);
+ if (t) {
+ return t;
+ }
+
+ if (A.ranked && !B.ranked) {
+ return -1;
+ }
+ if (!A.ranked && B.ranked) {
+ return 1;
+ }
+
+ return A.challenge_id - B.challenge_id;
+}
+
+export function time_per_move_challenge_sort(A: Challenge, B: Challenge) {
+ const comparison = Math.sign(A.time_per_move - B.time_per_move);
+
+ if (comparison) {
+ return comparison;
+ }
+
+ if (A.eligible && !B.eligible) {
+ return -1;
+ }
+ if (!A.eligible && B.eligible) {
+ return 1;
+ }
+ if (A.user_challenge && !B.user_challenge) {
+ return -1;
+ }
+ if (!A.user_challenge && B.user_challenge) {
+ return 1;
+ }
+
+ const createdA = A.created ? new Date(A.created).getTime() : -Infinity;
+ const createdB = B.created ? new Date(B.created).getTime() : -Infinity;
+ return createdA - createdB;
+}
diff --git a/src/views/Settings/GeneralPreferences.tsx b/src/views/Settings/GeneralPreferences.tsx
index 413fb353ce..3bd3cce300 100644
--- a/src/views/Settings/GeneralPreferences.tsx
+++ b/src/views/Settings/GeneralPreferences.tsx
@@ -49,7 +49,6 @@ export function GeneralPreferences(props: SettingGroupPageProps): JSX.Element {
usePreference("desktop-notifications-require-interaction");
*/
const [show_offline_friends, setShowOfflineFriends] = usePreference("show-offline-friends");
- const [show_seek_graph, setShowSeekGraph] = usePreference("show-seek-graph");
const [unicode_filter_usernames, setUnicodeFilterUsernames] = usePreference("unicode-filter");
const [translation_dialog_never_show, setTranslationDialogNeverShow] = usePreference(
"translation-dialog-never-show",
@@ -263,10 +262,6 @@ export function GeneralPreferences(props: SettingGroupPageProps): JSX.Element {
)*/}
-
-
-
-