import * as React from "react"; import { Button } from "../controls/Button"; import { EditorToggle } from "../controls/EditorToggle"; import { Input } from "../controls/Input"; import { Textarea } from "../controls/Textarea"; import { Modal } from "../controls/Modal"; import { ShareData } from "./Share"; import { ThumbnailRecorder } from "./ThumbnailRecorder"; import { SocialButton } from "./SocialButton"; import { Checkbox } from "../controls/Checkbox"; import { SimRecorder } from "./ThumbnailRecorder"; import { MultiplayerConfirmation } from "./MultiplayerConfirmation"; import { addGameToKioskAsync } from "./Kiosk"; import { pushNotificationMessage } from "../Notification"; import { classList } from "../util"; import { Link } from "../controls/Link"; const vscodeDevUrl = "https://vscode.dev/edu/makecode/" export interface ShareInfoProps { projectName: string; projectDescription?: string; screenshotUri?: string; isLoggedIn?: boolean; hasProjectBeenPersistentShared?: boolean; simRecorder: SimRecorder; publishAsync: (name: string, description?: string, screenshotUri?: string, forceAnonymous?: boolean) => Promise; isMultiplayerGame?: boolean; // Arcade: Does the game being shared have multiplayer enabled? kind?: "multiplayer" | "vscode" | "share"; // Arcade: Was the share dialog opened specifically for hosting a multiplayer game? anonymousShareByDefault?: boolean; setAnonymousSharePreference?: (anonymousByDefault: boolean) => void; onClose: () => void; } export const ShareInfo = (props: ShareInfoProps) => { const { projectName, projectDescription, screenshotUri, isLoggedIn, simRecorder, publishAsync, hasProjectBeenPersistentShared, anonymousShareByDefault, setAnonymousSharePreference, isMultiplayerGame, kind = "share", onClose, } = props; const [ name, setName ] = React.useState(projectName); const [ description, setDescription ] = React.useState(projectDescription); const [ thumbnailUri, setThumbnailUri ] = React.useState(screenshotUri); const [ shareState, setShareState ] = React.useState<"share" | "gifrecord" | "publish" | "publish-vscode" | "publishing">("share"); const [ shareData, setShareData ] = React.useState(); const [ lastShareWasAnonymous, setLastShareWasAnonymous ] = React.useState(undefined); const [ embedState, setEmbedState ] = React.useState<"none" | "code" | "editor" | "simulator">("none"); const [ showQRCode, setShowQRCode ] = React.useState(false); const [ copySuccessful, setCopySuccessful ] = React.useState(false); const [ kioskSubmitSuccessful, setKioskSubmitSuccessful ] = React.useState(false); const [ kioskState, setKioskState ] = React.useState(false); const [ isAnonymous, setIsAnonymous ] = React.useState(!isLoggedIn || anonymousShareByDefault); const [ isShowingMultiConfirmation, setIsShowingMultiConfirmation ] = React.useState(false); const { simScreenshot, simGif } = pxt.appTarget.appTheme; const showSimulator = (simScreenshot || simGif) && !!simRecorder; const prePublish = shareState === "share" || shareState === "publishing"; const isPublished = shareState === "publish" || shareState === "publish-vscode"; const showDescription = !isPublished; let qrCodeButtonRef: HTMLButtonElement; let inputRef: HTMLInputElement; let kioskInputRef: HTMLInputElement; React.useEffect(() => { setThumbnailUri(screenshotUri) }, [screenshotUri]) React.useEffect(() => { if (isLoggedIn) { pxt.tickEvent("share.open.loggedIn", { state: shareState, anonymous: isAnonymous?.toString(), persistent: hasProjectBeenPersistentShared?.toString() }); } else { pxt.tickEvent("share.open", { state: shareState}); } }, [shareState, isAnonymous, hasProjectBeenPersistentShared]); const exitGifRecord = () => { setShareState("share"); } const applyGifChange = (uri: string) => { setThumbnailUri(uri); exitGifRecord(); } const handlePublishClick = async () => { setShareState("publishing"); setLastShareWasAnonymous(isAnonymous); let publishedShareData = await publishAsync(name, description, thumbnailUri, isAnonymous); setShareData(publishedShareData); if (!publishedShareData?.error) setShareState("publish"); else setShareState("share") } const handlePublishInVscodeClick = async () => { setShareState("publishing"); setLastShareWasAnonymous(isAnonymous); let publishedShareData = await publishAsync(name, description, thumbnailUri, isAnonymous); setShareData(publishedShareData); if (!publishedShareData?.error) { setShareState("publish-vscode"); pxt.tickEvent(`share.openInVscode`); window.open(vscodeDevUrl + publishedShareData.url.split("/").pop(), "_blank"); } else setShareState("share") } const handleCopyClick = () => { if (pxt.BrowserUtils.isIpcRenderer()) { setCopySuccessful(pxt.BrowserUtils.legacyCopyText(inputRef)); } else { navigator.clipboard.writeText(shareData.url); setCopySuccessful(true); } } const handleCopyBlur = () => { setCopySuccessful(false); } const handleKioskSubmitBlur = () => { setKioskSubmitSuccessful(false); } const handleKioskSubmitClick = async () => { pxt.tickEvent("share.kiosk.submitClicked"); const gameId = pxt.Cloud.parseScriptId(shareData.url); if (kioskInputRef?.value) { let validKioskId = /^[a-zA-Z0-9]{6}$/.exec(kioskInputRef.value)?.[0]; if (validKioskId) { validKioskId = validKioskId.toUpperCase(); setKioskSubmitSuccessful(true); try { await addGameToKioskAsync(validKioskId, gameId); pxt.tickEvent("share.kiosk.submitSuccessful"); pushNotificationMessage({ kind: 'info', text: lf("Game submitted to kiosk {0} successfully!", validKioskId), hc: false }) } catch (error) { pxt.tickEvent("share.kiosk.submitServerError"); if (error.message === "Not Found") { pushNotificationMessage({ kind: 'err', text: lf("Kiosk Code not found"), hc: false }); } else { pushNotificationMessage({ kind: 'err', text: lf("Something went wrong submitting game to kiosk {0}", validKioskId), hc: false }); } } } else { pushNotificationMessage({ kind: 'err', text: lf("Invalid format for Kiosk Code"), hc: false }); } } else { pushNotificationMessage({ kind: 'err', text: lf("Input a six-character kiosk Code"), hc: false }); } } const handleEmbedClick = () => { if (embedState === "none") { pxt.tickEvent(`share.embed`); setShowQRCode(false); setKioskState(false); setEmbedState("code"); } else { setEmbedState("none"); } } const handleKioskClick = () => { if (!kioskState) { pxt.tickEvent(`share.kiosk`); setEmbedState("none"); setShowQRCode(false); setKioskState(true); } else { setKioskState(false); } } const handleKioskHelpClick = () => { const kioskDocumentationUrl = "https://arcade.makecode.com/hardware/kiosk"; window.open(kioskDocumentationUrl, "_blank"); } const handleQRCodeClick = () => { pxt.tickEvent('share.qrtoggle'); if (!showQRCode) { setEmbedState("none"); setShowQRCode(true); setKioskState(false); } else { setShowQRCode(false); } } const handleDeviceShareClick = async () => { pxt.tickEvent("share.device"); const shareOpts = { title: document.title, url: shareData.url, text: lf("Check out my new MakeCode project!"), }; // TODO: Fix this; typing for navigator not included in the lib typing we use in tsconfig if ((navigator as any)?.canShare?.(shareOpts)) { return navigator.share(shareOpts); } }; const handleMultiplayerShareConfirmClick = async () => { setShareState("publishing"); setIsShowingMultiConfirmation(false); setLastShareWasAnonymous(isAnonymous); const publishedShareData = await publishAsync(name, description, thumbnailUri, isAnonymous); // TODO multiplayer: This won't work on staging (parseScriptId domains check doesn't include staging urls) // but those wouldn't load anyways (as staging multiplayer is currently fetching games from prod links) const shareId = pxt.Cloud.parseScriptId(publishedShareData.url); if (!shareId) { pxt.tickEvent(`share.hostMultiplayerError`); return; } const multiplayerHostUrl = pxt.multiplayer.makeHostLink(shareId, false); // NOTE: It is allowable to log the shareId here because this is within the multiplayer context. // In this context, the user has consented to allowing the shareId being made public. pxt.tickEvent(`share.hostMultiplayerShared`, { shareId }); window.open(multiplayerHostUrl, "_blank"); setShareData(publishedShareData); if (!publishedShareData?.error) setShareState("publish"); else setShareState("share") if (kind === "multiplayer") { // If we're in the "for multiplayer" context, we want to close the share dialog after launching the multiplayer session. onClose(); } } const handleMultiplayerShareClick = async () => { setIsShowingMultiConfirmation(true); pxt.tickEvent(`share.hostMultiplayer`); } const handleMultiplayerShareCancelClick = async () => { setIsShowingMultiConfirmation(false); pxt.tickEvent(`share.hostMultiplayerCancel`); } const embedOptions = [{ name: "code", label: lf("Code"), title: lf("Code"), focusable: true, onClick: () => setEmbedState("code") }, { name: "editor", label: lf("Editor"), title: lf("Editor"), focusable: true, onClick: () => setEmbedState("editor") }, { name: "simulator", label: lf("Simulator"), title: lf("Simulator"), focusable: true, onClick: () => setEmbedState("simulator") }]; const handleQRCodeButtonRef = (ref: HTMLButtonElement) => { if (ref) qrCodeButtonRef = ref; } const handleQRCodeModalClose = () => { setShowQRCode(false); if (qrCodeButtonRef) qrCodeButtonRef.focus(); } const handleInputRef = (ref: HTMLInputElement) => { if (ref) inputRef = ref; } const handleKioskInputRef = (ref: HTMLInputElement) => { if (ref) kioskInputRef = ref; } const handleAnonymousShareClick = (newValue: boolean) => { pxt.tickEvent("share.persistentCheckbox", { checked: newValue.toString() }); setIsAnonymous(!newValue); if (setAnonymousSharePreference) setAnonymousSharePreference(!newValue); } const inputTitle = prePublish ? lf("Project Name") : (shareState === "publish-vscode" ? lf("Share Successful") : lf("Project Link")); const shareAttemptWasAnonymous = lastShareWasAnonymous === undefined ? isAnonymous : lastShareWasAnonymous; const hasGithubProvider = !!pxt.appTarget?.cloud?.cloudProviders?.github; const shareErrorMessage = (() => { if (!shareData?.error) return undefined; if (shareData.error.statusCode !== 413) { return lf("Oops! There was an error. Please ensure you are connected to the Internet and try again."); } if (shareAttemptWasAnonymous && hasGithubProvider) { return lf("Oops! Your project is too big to share anonymously. Sign in and create a persistent link for higher limits, or publish to GitHub instead."); } else if (shareAttemptWasAnonymous) { return lf("Oops! Your project is too big to share anonymously. Sign in and create a persistent link to unlock a higher size limit."); } else if (hasGithubProvider) { return lf("Oops! Your project is too big. You can create a GitHub repository to share it."); } else { return lf("Oops! Your project is too big to share."); } })(); return <>
{showSimulator && shareState !== "gifrecord" &&
{thumbnailUri ? {lf("Preview :
} {!isPublished &&
}
{(prePublish || isPublished) && <>
{inputTitle}
{showDescription && <> {pxt.appTarget.appTheme.showProjectDescription && <>
{lf("Project Description")}