Compare commits

...

4 Commits

Author SHA1 Message Date
Davy Hélard
e93e65e7e1 Review changes: alert and parallelism 2022-04-12 14:25:41 +02:00
Davy Hélard
b67e7035bc Rewording of the tip. 2022-04-12 14:25:41 +02:00
Davy Hélard
b0f824f889 Disable slugs fields and add a tip when the user doesn't have a username. 2022-04-12 14:25:41 +02:00
Davy Hélard
ee7065d52c Allow to verify and change the url slug right before publishing a game 2022-04-12 14:22:52 +02:00
5 changed files with 162 additions and 43 deletions

View File

@@ -38,7 +38,10 @@ import {
getGame, getGame,
getGameUrl, getGameUrl,
updateGame, updateGame,
setGameSlug,
getGameSlugs,
type Game, type Game,
type GameSlug,
} from '../../../Utils/GDevelopServices/Game'; } from '../../../Utils/GDevelopServices/Game';
import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext'; import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext';
import AlertMessage from '../../../UI/AlertMessage'; import AlertMessage from '../../../UI/AlertMessage';
@@ -78,6 +81,7 @@ const OnlineGameLink = ({
setIsOnlineGamePropertiesDialogOpen, setIsOnlineGamePropertiesDialogOpen,
] = React.useState<boolean>(false); ] = React.useState<boolean>(false);
const [game, setGame] = React.useState<?Game>(null); const [game, setGame] = React.useState<?Game>(null);
const [slug, setSlug] = React.useState<?GameSlug>(null);
const [isGameLoading, setIsGameLoading] = React.useState<boolean>(false); const [isGameLoading, setIsGameLoading] = React.useState<boolean>(false);
const { getAuthorizationHeader, profile } = React.useContext( const { getAuthorizationHeader, profile } = React.useContext(
AuthenticatedUserContext AuthenticatedUserContext
@@ -86,7 +90,7 @@ const OnlineGameLink = ({
const exportPending = !errored && exportStep !== '' && exportStep !== 'done'; const exportPending = !errored && exportStep !== '' && exportStep !== 'done';
const isBuildComplete = build && build.status === 'complete'; const isBuildComplete = build && build.status === 'complete';
const isBuildPublished = build && game && build.id === game.publicWebBuildId; const isBuildPublished = build && game && build.id === game.publicWebBuildId;
const gameUrl = getGameUrl(game); const gameUrl = getGameUrl(game, slug);
const buildUrl = const buildUrl =
exportPending || !isBuildComplete exportPending || !isBuildComplete
? null ? null
@@ -102,8 +106,16 @@ const OnlineGameLink = ({
const { id } = profile; const { id } = profile;
try { try {
setIsGameLoading(true); setIsGameLoading(true);
const game = await getGame(getAuthorizationHeader, id, gameId); const [game, slugs] = await Promise.all([
getGame(getAuthorizationHeader, id, gameId),
getGameSlugs(getAuthorizationHeader, id, gameId).catch(err => {
console.error('Unable to get the game slug', err);
}),
]);
setGame(game); setGame(game);
if (slugs && slugs.length > 0) {
setSlug(slugs[0]);
}
} catch (err) { } catch (err) {
console.error('Unable to load the game', err); console.error('Unable to load the game', err);
} finally { } finally {
@@ -189,6 +201,35 @@ const OnlineGameLink = ({
} }
); );
setGame(updatedGame); setGame(updatedGame);
const { userSlug, gameSlug } = partialGameChange;
if (userSlug && gameSlug && userSlug === profile.username) {
try {
await setGameSlug(
getAuthorizationHeader,
id,
game.id,
userSlug,
gameSlug
);
setSlug({ username: userSlug, gameSlug: gameSlug, createdAt: 0 });
} catch (error) {
console.error(
'Unable to update the game slug:',
error.response || error.message
);
showErrorBox({
message:
i18n._(
t`Unable to update the game slug. A slug must be 6 to 30 characters long and only contains letters, digits or dashes.`
) +
' ' +
i18n._(t`Verify your internet connection or try again later.`),
rawError: error,
errorId: 'game-slug-update-error',
});
return false;
}
}
} catch (err) { } catch (err) {
showErrorBox({ showErrorBox({
message: i18n._( message: i18n._(
@@ -377,6 +418,7 @@ const OnlineGameLink = ({
} }
}} }}
game={game} game={game}
slug={slug}
isLoading={isGameLoading} isLoading={isGameLoading}
/> />
)} )}

View File

@@ -2,22 +2,27 @@
import { t, Trans } from '@lingui/macro'; import { t, Trans } from '@lingui/macro';
import * as React from 'react'; import * as React from 'react';
import { type Game } from '../../../Utils/GDevelopServices/Game'; import { type Game, type GameSlug } from '../../../Utils/GDevelopServices/Game';
import FlatButton from '../../../UI/FlatButton'; import FlatButton from '../../../UI/FlatButton';
import Dialog from '../../../UI/Dialog'; import Dialog from '../../../UI/Dialog';
import PublicGameProperties from '../../../GameDashboard/PublicGameProperties'; import {
cleanUpGameSlug,
PublicGameProperties,
} from '../../../GameDashboard/PublicGameProperties';
import { import {
applyPublicPropertiesToProject, applyPublicPropertiesToProject,
type PartialGameChange, type PartialGameChange,
} from '../../../GameDashboard/PublicGamePropertiesDialog'; } from '../../../GameDashboard/PublicGamePropertiesDialog';
import { getWebBuildThumbnailUrl } from '../../../Utils/GDevelopServices/Build'; import { getWebBuildThumbnailUrl } from '../../../Utils/GDevelopServices/Build';
import RaisedButtonWithSplitMenu from '../../../UI/RaisedButtonWithSplitMenu'; import RaisedButtonWithSplitMenu from '../../../UI/RaisedButtonWithSplitMenu';
import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext';
type Props = {| type Props = {|
project: gdProject, project: gdProject,
onSaveProject: () => Promise<void>, onSaveProject: () => Promise<void>,
buildId: string, buildId: string,
game: Game, game: Game,
slug: ?GameSlug,
onClose: () => void, onClose: () => void,
onApply: PartialGameChange => Promise<void>, onApply: PartialGameChange => Promise<void>,
isLoading: boolean, isLoading: boolean,
@@ -28,10 +33,13 @@ export const OnlineGamePropertiesDialog = ({
onSaveProject, onSaveProject,
buildId, buildId,
game, game,
slug,
onClose, onClose,
onApply, onApply,
isLoading, isLoading,
}: Props) => { }: Props) => {
const { profile } = React.useContext(AuthenticatedUserContext);
const [name, setName] = React.useState<string>(project.getName()); const [name, setName] = React.useState<string>(project.getName());
const [categories, setCategories] = React.useState<string[]>( const [categories, setCategories] = React.useState<string[]>(
project.getCategories().toJSArray() project.getCategories().toJSArray()
@@ -51,6 +59,12 @@ export const OnlineGamePropertiesDialog = ({
const [playWithMobile, setPlayableWithMobile] = React.useState<boolean>( const [playWithMobile, setPlayableWithMobile] = React.useState<boolean>(
project.isPlayableWithMobile() project.isPlayableWithMobile()
); );
const [userSlug, setUserSlug] = React.useState<string>(
(slug && slug.username) || (profile && profile.username) || ''
);
const [gameSlug, setGameSlug] = React.useState<string>(
(slug && slug.gameSlug) || cleanUpGameSlug(project.getName())
);
const [orientation, setOrientation] = React.useState<string>( const [orientation, setOrientation] = React.useState<string>(
project.getOrientation() project.getOrientation()
); );
@@ -78,7 +92,7 @@ export const OnlineGamePropertiesDialog = ({
orientation: orientation || 'default', orientation: orientation || 'default',
}) })
) { ) {
await onApply({ discoverable }); await onApply({ discoverable, userSlug, gameSlug });
} }
}; };
@@ -131,6 +145,10 @@ export const OnlineGamePropertiesDialog = ({
playWithMobile={playWithMobile} playWithMobile={playWithMobile}
setOrientation={setOrientation} setOrientation={setOrientation}
orientation={orientation} orientation={orientation}
userSlug={userSlug}
setUserSlug={setUserSlug}
gameSlug={gameSlug}
setGameSlug={setGameSlug}
discoverable={discoverable} discoverable={discoverable}
setDiscoverable={setDiscoverable} setDiscoverable={setDiscoverable}
displayThumbnail displayThumbnail

View File

@@ -17,6 +17,7 @@ import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
import { I18n } from '@lingui/react'; import { I18n } from '@lingui/react';
import { Column, Line, Spacer } from '../UI/Grid'; import { Column, Line, Spacer } from '../UI/Grid';
import BackgroundText from '../UI/BackgroundText'; import BackgroundText from '../UI/BackgroundText';
import AlertMessage from '../UI/AlertMessage';
import { GameThumbnail } from './GameThumbnail'; import { GameThumbnail } from './GameThumbnail';
const isCyrillic = (text: string) => const isCyrillic = (text: string) =>
@@ -195,42 +196,68 @@ export function PublicGameProperties({
rows={5} rows={5}
/> />
{setUserSlug && setGameSlug && ( {setUserSlug && setGameSlug && (
<Line> <>
<SelectField <Line>
fullWidth <SelectField
floatingLabelText={<Trans>User name in the game URL</Trans>} fullWidth
value={userSlug || ''} floatingLabelText={<Trans>User name in the game URL</Trans>}
onChange={(e, i, value: string) => setUserSlug(value)} value={userSlug || ''}
// It's disabled if one of the condition of SelectOption is false. onChange={(e, i, value: string) => setUserSlug(value)}
disabled={ // It's disabled if one of the condition of SelectOption is false.
!( disabled={
profile && !(
profile.username && profile &&
userSlug && profile.username &&
(!profile || userSlug !== profile.username) userSlug &&
) (!profile || userSlug !== profile.username)
} )
> }
{profile && profile.username && ( >
<SelectOption {profile && profile.username && (
value={profile.username} <SelectOption
primaryText={profile.username} value={profile.username}
/> primaryText={profile.username}
)} />
{userSlug && (!profile || userSlug !== profile.username) && ( )}
<SelectOption value={userSlug} primaryText={userSlug} /> {userSlug && (!profile || userSlug !== profile.username) && (
)} <SelectOption value={userSlug} primaryText={userSlug} />
</SelectField> )}
<Spacer /> </SelectField>
<SemiControlledTextField <Spacer />
floatingLabelText={<Trans>Game name in the game URL</Trans>} <SemiControlledTextField
fullWidth disabled={
type="text" !(
value={gameSlug || ''} userSlug &&
onChange={gameSlug => setGameSlug(cleanUpGameSlug(gameSlug))} userSlug.length &&
autoFocus profile &&
/> profile.username
</Line> )
}
floatingLabelText={<Trans>Game name in the game URL</Trans>}
fullWidth
type="text"
value={
userSlug && userSlug.length && profile && profile.username
? gameSlug || ''
: ''
}
onChange={gameSlug => setGameSlug(cleanUpGameSlug(gameSlug))}
autoFocus
/>
</Line>
{!(
userSlug &&
userSlug.length &&
profile &&
profile.username
) && (
<AlertMessage kind="info">
<Trans>
Usernames are required to choose a custom game URL.
</Trans>
</AlertMessage>
)}
</>
)} )}
<UsersAutocomplete <UsersAutocomplete
userIds={authorIds} userIds={authorIds}

View File

@@ -16,6 +16,10 @@ export const GDevelopGamesPlatform = {
isDev isDev
? `https://liluo.io/games/${gameId}?dev=true` ? `https://liluo.io/games/${gameId}?dev=true`
: `https://liluo.io/games/${gameId}`, : `https://liluo.io/games/${gameId}`,
getGameUrlWithSlug: (userSlug: string, gameSlug: string) =>
isDev
? `https://liluo.io/${userSlug.toLowerCase()}/${gameSlug.toLowerCase()}?dev=true`
: `https://liluo.io/${userSlug.toLowerCase()}/${gameSlug.toLowerCase()}`,
getUserPublicProfileUrl: (userId: string, username: ?string) => getUserPublicProfileUrl: (userId: string, username: ?string) =>
username username
? `https://liluo.io/${username}${isDev ? '?dev=true' : ''}` ? `https://liluo.io/${username}${isDev ? '?dev=true' : ''}`

View File

@@ -38,6 +38,12 @@ export type Game = {
discoverable?: boolean, discoverable?: boolean,
}; };
export type GameSlug = {
username: string,
gameSlug: string,
createdAt: number,
};
export type ShowcasedGameLink = { export type ShowcasedGameLink = {
url: string, url: string,
type: type:
@@ -129,9 +135,11 @@ export const getCategoryName = (category: string, i18n: I18nType) => {
} }
}; };
export const getGameUrl = (game: ?Game) => { export const getGameUrl = (game: ?Game, slug: ?GameSlug) => {
if (!game) return null; if (!game) return null;
return GDevelopGamesPlatform.getGameUrl(game.id); return slug
? GDevelopGamesPlatform.getGameUrlWithSlug(slug.username, slug.gameSlug)
: GDevelopGamesPlatform.getGameUrl(game.id);
}; };
export const getAclsFromUserIds = ( export const getAclsFromUserIds = (
@@ -374,3 +382,23 @@ export const getPublicGame = (gameId: string): Promise<PublicGame> => {
.get(`${GDevelopGameApi.baseUrl}/public-game/${gameId}`) .get(`${GDevelopGameApi.baseUrl}/public-game/${gameId}`)
.then(response => response.data); .then(response => response.data);
}; };
export const getGameSlugs = (
getAuthorizationHeader: () => Promise<string>,
userId: string,
gameId: string
): Promise<Array<GameSlug>> => {
return getAuthorizationHeader()
.then(authorizationHeader =>
axios.get(`${GDevelopGameApi.baseUrl}/game-slug`, {
params: {
userId,
gameId,
},
headers: {
Authorization: authorizationHeader,
},
})
)
.then(response => response.data);
};