Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Deep Links for Copying Multiplayer Join Code, Update Host Lobby Appearance #9153

Merged
merged 14 commits into from
Oct 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion multiplayer/src/components/ArcadeSimulator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,9 @@ export default function Render() {
<div
id="sim-container"
ref={simContainerRef}
className={"tw-h-[calc(100vh-16rem)] tw-w-[calc(100vw-6rem)]"}
className={
"tw-h-[calc(100vh-16rem)] tw-w-screen md:tw-w-[calc(100vw-6rem)]"
}
/>
);
}
53 changes: 53 additions & 0 deletions multiplayer/src/components/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { faCopy } from "@fortawesome/free-regular-svg-icons";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useContext, useEffect, useState } from "react";
import { AppStateContext } from "../state/AppStateContext";

export default function Render(props: {
copyValue: string;
title: string;
eventName?: string;
}) {
const { state } = useContext(AppStateContext);
const [copySuccessful, setCopySuccessful] = useState(false);
const copyTimeoutMs = 2500;

const copyValue = async () => {
if (props.eventName) pxt.tickEvent(props.eventName);
if (state.gameState?.joinCode) {
navigator.clipboard.writeText(props.copyValue);
setCopySuccessful(true);
}
};

useEffect(() => {
if (copySuccessful) {
let resetCopyTimer = setTimeout(() => {
setCopySuccessful(false);
}, copyTimeoutMs);
return () => {
clearTimeout(resetCopyTimer);
};
}
}, [copySuccessful]);

return (
<button onClick={copyValue} title={props.title}>
<div>
{!copySuccessful && (
<FontAwesomeIcon
icon={faCopy}
className="hover:tw-scale-110 tw-ease-linear tw-duration-[50ms]"
/>
)}
{copySuccessful && (
<FontAwesomeIcon
icon={faCheck}
className="tw-text-green-600"
/>
)}
</div>
</button>
);
}
75 changes: 37 additions & 38 deletions multiplayer/src/components/HostLobby.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,64 @@
import { useContext, useRef, useState } from "react";
import { Button } from "react-common/components/controls/Button";
import { Input } from "react-common/components/controls/Input";
import { Link } from "react-common/components/controls/Link";
import { startGameAsync } from "../epics";
import { clearModal } from "../state/actions";
import { AppStateContext } from "../state/AppStateContext";
import { makeJoinLink, SHORT_LINK } from "../util";
import CopyButton from "./CopyButton";
import Loading from "./Loading";
import PresenceBar from "./PresenceBar";

export default function Render() {
const { state, dispatch } = useContext(AppStateContext);
const [copySuccessful, setCopySuccessful] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const inviteString = lf("Invite anyone to join your game instantly. Just send them a link!");

const onStartGameClick = async () => {
pxt.tickEvent("mp.hostlobby.startgame");
dispatch(clearModal());
await startGameAsync();
};

const handleCopyClick = () => {
pxt.tickEvent("mp.hostlobby.copyjoinlink");
if (pxt.BrowserUtils.isIpcRenderer()) {
if (inputRef.current) {
setCopySuccessful(
pxt.BrowserUtils.legacyCopyText(inputRef.current)
);
}
} else {
navigator.clipboard.writeText(joinLink);
setCopySuccessful(true);
}
};
const joinCode = state.gameState?.joinCode;
if (!joinCode) {
return null;
}

const handleCopyBlur = () => {
setCopySuccessful(false);
};
// To get a link in the middle of the invite string, we actually preserve the {0} and insert the link manually as an html element later.
const inviteString = lf("Go to {0} and enter code", "{0}");
const inviteStringSegments = inviteString.split("{0}");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice :)

const shortLink = SHORT_LINK();

// Insert a space to make the join code easier to read.
const displayJoinCode = joinCode?.slice(0, 3) + " " + joinCode?.slice(3);
const joinDeepLink = makeJoinLink(joinCode);

const joinLink = `${state.gameState?.joinCode}`; // TODO multiplayer : create full link
return (
<div className="tw-flex tw-flex-col tw-gap-1 tw-items-center tw-justify-between tw-bg-white tw-py-[3rem] tw-px-[7rem] tw-shadow-lg tw-rounded-lg">
<div className="tw-mt-3 tw-text-lg tw-text-center tw-text-neutral-700">
{inviteString}
{inviteStringSegments[0]}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming the number of elements in inviteStringSegments feels brittle to me. And assuming the link is in the middle segment isn't a safe assumption. I think you will need to iterate over the segments, and replace the one matching "{0}" with the link.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, currently we split on {0} so there's no actual segment containing it. The only scenario where there wouldn't be 2 segments is if it was at the beginning or the end, so would it be better if we simply checked startsWith and endsWith {0} and reacted accordingly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess if {0} ended up in multiple places that'd also mess things up but I don't expect that would happen as a result of localization.

Copy link
Member

@jwunderl jwunderl Oct 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

split will leave an empty string at beginning / end if the the string starts with / ends with it so that would still be fine, e.g.
image
I guess if the translation doubled up the link it would be an issue, but that would be pretty weird / we do this in a few other locations already and haven't seen that before?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would split on whitespace and find the {0}. You could optimize by rejoining the contiguous word sequences before and after it. Do this in a useMemo.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But there a ways of solving it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the benefit? Feels like unnecessary complexity to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this as-is for now b/c time crunch, but we can follow up later to understand concerns and address as needed.

{
<Link
href={shortLink}
target="_blank"
className="tw-text-primary-color tw-font-bold hover:tw-text-orange-300"
>
{shortLink}
</Link>
}
{inviteStringSegments[1]}
</div>
<div className="common-input-attached-button tw-mt-5 tw-w-full">
<Input
ariaLabel={lf("join game link")}
handleInputRef={inputRef}
initialValue={joinLink}
readOnly={true}
/>
<Button
className={copySuccessful ? "green" : "primary"}
title={lf("Copy link")}
label={copySuccessful ? lf("Copied!") : lf("Copy")}
leftIcon="fas fa-link"
onClick={handleCopyClick}
onBlur={handleCopyBlur}
/>
<div className="tw-text-4xl tw-mt-4 tw-flex tw-flex-row tw-items-center">
{displayJoinCode}
<div className="tw-ml-2 tw-text-[75%]">
<CopyButton
copyValue={joinDeepLink}
title={lf("Copy join link")}
eventName="mp.hostlobby.copyjoinlink"
/>
</div>
</div>
<Button
className={"teal tw-m-5"}
className={"primary tw-mt-5 tw-mb-7 tw-font-sans"}
label={lf("Start Game")}
title={lf("Start Game")}
onClick={onStartGameClick}
Expand Down
60 changes: 16 additions & 44 deletions multiplayer/src/components/JoinCodeLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,26 @@
import { faCopy } from "@fortawesome/free-regular-svg-icons";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useContext, useEffect, useState } from "react";
import { AppStateContext } from "../state/AppStateContext";
import { makeJoinLink } from "../util";
import CopyButton from "./CopyButton";

export default function Render() {
const { state } = useContext(AppStateContext);
const [copySuccessful, setCopySuccessful] = useState(false);
const copyTimeoutMs = 2500;

const copyJoinCode = async () => {
pxt.tickEvent("mp.copyjoincode");
if (state.gameState?.joinCode) {
navigator.clipboard.writeText(state.gameState?.joinCode);
setCopySuccessful(true);
}
};

useEffect(() => {
if (copySuccessful) {
let resetCopyTimer = setTimeout(() => {
setCopySuccessful(false);
}, copyTimeoutMs);
return () => {
clearTimeout(resetCopyTimer);
};
}
}, [copySuccessful]);

const joinCode = state.gameState?.joinCode;
const joinDeepLink = joinCode ? makeJoinLink(joinCode) : "";
return (
<div className="tw-justify-self-center">
{state.gameState?.joinCode && (
<div>
{lf("Join Code: {0}", state.gameState?.joinCode)}
<button onClick={copyJoinCode} title={lf("Copy Join Code")}>
<div className="tw-text-sm tw-ml-1">
{!copySuccessful && (
<FontAwesomeIcon
icon={faCopy}
className="hover:tw-scale-110 tw-ease-linear tw-duration-[50ms] tw-mb-[0.1rem]"
/>
)}
{copySuccessful && (
<FontAwesomeIcon
icon={faCheck}
className="tw-text-green-600 tw-mb-[0.1rem]"
/>
)}
</div>
</button>
<div>
{joinCode && (
<div className="tw-flex tw-flex-row tw-items-center tw-align-middle">
<div className="tw-font-bold">{lf("Code:")}</div>
<div className="tw-mx-1">{joinCode}</div>
<div className="tw-text-[75%]">
<CopyButton
copyValue={joinDeepLink}
title={lf("Copy join link")}
eventName="mp.copyjoinlink"
/>
</div>
</div>
)}
</div>
Expand Down
2 changes: 0 additions & 2 deletions multiplayer/src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@ export function cleanupJoinCode(
joinCode: string | undefined
): string | undefined {
if (!joinCode) return undefined;
joinCode = joinCode.trim();
if (joinCode.length !== 6) return undefined;
joinCode = joinCode.toUpperCase().replace(/[^A-Z0-9]/g, "");
if (joinCode.length !== 6) return undefined;
return joinCode;
Expand Down