Improve web export with multiple sharing capabilities

This commit is contained in:
Clément Pasteau
2021-12-15 10:25:41 +01:00
committed by GitHub
parent a4d0c591a8
commit 9163e998f9
8 changed files with 265 additions and 51141 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -70,6 +70,7 @@
"react-measure": "2.3.0",
"react-monaco-editor": "^0.18.0",
"react-mosaic-component": "git://github.com/4ian/react-mosaic#v3.1.0",
"react-share": "^4.4.0",
"react-sortable-hoc": "1.5.0",
"react-sortable-tree": "2.6.2",
"react-test-renderer": "16.8.6",
@@ -109,13 +110,20 @@
"eslintConfig": {
"extends": "react-app",
"rules": {
"no-restricted-imports": ["error", {
"paths": [{
"name": "@lingui/react",
"importNames": ["Trans"],
"message": "Please import Trans from @lingui/macro"
}]
}]
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "@lingui/react",
"importNames": [
"Trans"
],
"message": "Please import Trans from @lingui/macro"
}
]
}
]
}
},
"flow-coverage-report": {

View File

@@ -9,7 +9,7 @@ export default () => {
const theme = React.useContext(GDevelopThemeContext);
return (
windowWidth !== 'small' && (
<Column justifyContent="center">
<Column justifyContent="center" noMargin>
<span
style={{
height: 'calc(100% - 30px)',

View File

@@ -411,7 +411,7 @@ export default class ExportLauncher extends Component<Props, State> {
})}
{doneFooterOpen && (
<Line justifyContent="center">
<GameRegistration project={project} />
<GameRegistration project={project} hideIfSubscribed hideLoader />
</Line>
)}
</Column>

View File

@@ -3,7 +3,7 @@ import { Trans } from '@lingui/macro';
import { t } from '@lingui/macro';
import * as React from 'react';
import Text from '../../UI/Text';
import { Column, Line } from '../../UI/Grid';
import { Column, Line, Spacer } from '../../UI/Grid';
import TextField from '../../UI/TextField';
import {
getBuildArtifactUrl,
@@ -12,9 +12,30 @@ import {
import RaisedButton from '../../UI/RaisedButton';
import Window from '../../Utils/Window';
import Copy from '../../UI/CustomSvgIcons/Copy';
import Share from '@material-ui/icons/Share';
import InfoBar from '../../UI/Messages/InfoBar';
import IconButton from '../../UI/IconButton';
import { TextFieldWithButtonLayout } from '../../UI/Layout';
import { LinearProgress } from '@material-ui/core';
import FlatButton from '../../UI/FlatButton';
import Dialog from '../../UI/Dialog';
import {
EmailShareButton,
FacebookShareButton,
RedditShareButton,
TwitterShareButton,
WhatsappShareButton,
EmailIcon,
FacebookIcon,
RedditIcon,
TwitterIcon,
WhatsappIcon,
} from 'react-share';
const styles = {
icon: {
padding: 5,
},
};
export const ExplanationHeader = () => (
<Column noMargin alignItems="center" justifyContent="center">
@@ -38,62 +59,134 @@ export const WebProjectLink = ({ build, loading }: WebProjectLinkProps) => {
const [showCopiedInfoBar, setShowCopiedInfoBar] = React.useState<boolean>(
false
);
const [isShareDialogOpen, setIsShareDialogOpen] = React.useState<boolean>(
false
);
if (!build && !loading) return null;
const buildPending = loading || (build && build.status !== 'complete');
const value = buildPending
? 'Just a few seconds while we generate the link...'
: getBuildArtifactUrl(build, 's3Key') || '';
const buildUrl = buildPending ? null : getBuildArtifactUrl(build, 's3Key');
const onOpen = () => {
if (buildPending) return;
Window.openExternalURL(value);
if (buildPending || !buildUrl) return;
Window.openExternalURL(buildUrl);
};
const onCopy = () => {
if (buildPending) return;
if (buildPending || !buildUrl) return;
// TODO: use Clipboard.js, after it's been reworked to use this API and handle text.
navigator.clipboard.writeText(value);
navigator.clipboard.writeText(buildUrl);
setShowCopiedInfoBar(true);
};
const onShare = async () => {
if (buildPending || !buildUrl) return;
if (!navigator.share) {
// We are on desktop (or do not support sharing using the system dialog).
setIsShareDialogOpen(true);
return;
}
// We are on mobile (or on browsers supporting sharing using the system dialog).
const shareData = {
title: 'My GDevelop game',
text: 'Try the game I just created with #gdevelop',
url: buildUrl,
};
try {
await navigator.share(shareData);
} catch (err) {
console.error("Couldn't share the game", err);
}
};
return (
<>
<TextFieldWithButtonLayout
noFloatingLabelText
renderTextField={() => (
<TextField
value={value}
readOnly
fullWidth
endAdornment={
<IconButton
disabled={!!buildPending}
onClick={onCopy}
tooltip={t`Copy`}
edge="end"
{buildPending ? (
<>
<Text>
<Trans>Just a few seconds while we generate the link...</Trans>
</Text>
<LinearProgress />
</>
) : (
<Line justifyContent="center">
<FlatButton
label={<Trans>Share</Trans>}
onClick={onShare}
icon={<Share />}
/>
<Spacer />
<RaisedButton label={<Trans>Open</Trans>} onClick={onOpen} primary />
</Line>
)}
<Dialog
title={<Trans>Share your game</Trans>}
actions={[
<FlatButton
key="close"
label={<Trans>Back</Trans>}
primary={false}
onClick={() => setIsShareDialogOpen(false)}
/>,
]}
open={isShareDialogOpen}
onRequestClose={() => setIsShareDialogOpen(false)}
>
{buildUrl && (
<Column>
<TextField
value={buildUrl}
readOnly
fullWidth
endAdornment={
<IconButton onClick={onCopy} tooltip={t`Copy`} edge="end">
<Copy />
</IconButton>
}
/>
<Line justifyContent="flex-end">
<FacebookShareButton
url={`Try the game I just created with #gdevelop: ${buildUrl}`}
style={styles.icon}
>
<Copy />
</IconButton>
}
/>
<FacebookIcon size={32} round />
</FacebookShareButton>
<RedditShareButton
url={`Try the game I just created with r/gdevelop: ${buildUrl}`}
style={styles.icon}
>
<RedditIcon size={32} round />
</RedditShareButton>
<TwitterShareButton
url={`Try the game I just created with #gdevelop: ${buildUrl}`}
style={styles.icon}
>
<TwitterIcon size={32} round />
</TwitterShareButton>
<WhatsappShareButton
url={`Try the game I just created with gdevelop.io: ${buildUrl}`}
style={styles.icon}
>
<WhatsappIcon size={32} round />
</WhatsappShareButton>
<EmailShareButton
url={`Try the game I just created with gdevelop.io: ${buildUrl}`}
style={styles.icon}
>
<EmailIcon size={32} round />
</EmailShareButton>
</Line>
</Column>
)}
renderButton={style => (
<RaisedButton
disabled={!!buildPending}
primary
label={<Trans>Open</Trans>}
onClick={onOpen}
style={style}
/>
)}
/>
<InfoBar
message={<Trans>Copied to clipboard!</Trans>}
visible={showCopiedInfoBar}
hide={() => setShowCopiedInfoBar(false)}
/>
<InfoBar
message={<Trans>Copied to clipboard!</Trans>}
visible={showCopiedInfoBar}
hide={() => setShowCopiedInfoBar(false)}
/>
</Dialog>
</>
);
};

View File

@@ -23,6 +23,7 @@ import { GameDetailsDialog } from './GameDetailsDialog';
type Props = {|
project: ?gdProject,
hideIfRegistered?: boolean,
hideIfSubscribed?: boolean,
hideLoader?: boolean,
onGameRegistered?: () => void,
|};
@@ -33,6 +34,7 @@ type UnavailableReason = 'unauthorized' | 'not-existing' | null;
export const GameRegistration = ({
project,
hideIfRegistered,
hideIfSubscribed,
hideLoader,
onGameRegistered,
}: Props) => {
@@ -168,6 +170,7 @@ export const GameRegistration = ({
onRegisterGame={onRegisterGame}
registrationInProgress={registrationInProgress}
hideIfRegistered={hideIfRegistered}
hideIfSubscribed={hideIfSubscribed}
unavailableReason={unavailableReason}
acceptGameStatsEmailInProgress={acceptGameStatsEmailInProgress}
onAcceptGameStatsEmail={_onAcceptGameStatsEmail}
@@ -193,6 +196,7 @@ export type GameRegistrationWidgetProps = {|
onRegisterGame: () => Promise<void>,
registrationInProgress: boolean,
hideIfRegistered?: boolean,
hideIfSubscribed?: boolean,
unavailableReason: ?UnavailableReason,
acceptGameStatsEmailInProgress: boolean,
onAcceptGameStatsEmail: () => Promise<void>,
@@ -216,6 +220,7 @@ export const GameRegistrationWidget = ({
onRegisterGame,
registrationInProgress,
hideIfRegistered,
hideIfSubscribed,
unavailableReason,
acceptGameStatsEmailInProgress,
onAcceptGameStatsEmail,
@@ -302,6 +307,7 @@ export const GameRegistrationWidget = ({
</AlertMessage>
);
}
if (hideIfSubscribed) return null;
return (
<ColumnStackLayout noMargin>
<Line justifyContent="center">

View File

@@ -9,7 +9,7 @@ import { tooltipEnterDelay } from './Tooltip';
// They should be self descriptive - refer to Material UI docs otherwise.
type Props = {|
label: React.Node,
onClick: ?(ev: any) => void,
onClick: ?(ev: any) => void | Promise<void>,
primary?: boolean,
disabled?: boolean,
keyboardFocused?: boolean,

View File

@@ -11,6 +11,9 @@ import {
GameRegistrationWidget,
type GameRegistrationWidgetProps,
} from '../../GameDashboard/GameRegistration';
import GDevelopJsInitializerDecorator, {
testProject,
} from '../GDevelopJsInitializerDecorator';
import {
indieUserProfile,
game1,
@@ -22,13 +25,10 @@ const indieUserProfileWithGameStatsEmail: Profile = {
getGameStatsEmail: true,
};
// $FlowFixMe - Understand why the testProject imported from fixtures is always undefined.
const testProject: TestProject = { project: 'fake' };
export default {
title: 'GameDashboard/GameRegistrationWidget',
component: GameRegistrationWidget,
decorators: [paperDecorator, muiDecorator],
decorators: [paperDecorator, muiDecorator, GDevelopJsInitializerDecorator],
};
const defaultProps: GameRegistrationWidgetProps = {
@@ -94,6 +94,13 @@ export const EmailAccepted = () => (
profile={indieUserProfileWithGameStatsEmail}
/>
);
export const EmailAcceptedButHidingIfSubscribed = () => (
<GameRegistrationWidget
{...defaultProps}
profile={indieUserProfileWithGameStatsEmail}
hideIfSubscribed
/>
);
export const DetailsOpened = () => (
<GameRegistrationWidget
{...defaultProps}