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,
getGameUrl,
updateGame,
setGameSlug,
getGameSlugs,
type Game,
type GameSlug,
} from '../../../Utils/GDevelopServices/Game';
import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext';
import AlertMessage from '../../../UI/AlertMessage';
@@ -78,6 +81,7 @@ const OnlineGameLink = ({
setIsOnlineGamePropertiesDialogOpen,
] = React.useState<boolean>(false);
const [game, setGame] = React.useState<?Game>(null);
const [slug, setSlug] = React.useState<?GameSlug>(null);
const [isGameLoading, setIsGameLoading] = React.useState<boolean>(false);
const { getAuthorizationHeader, profile } = React.useContext(
AuthenticatedUserContext
@@ -86,7 +90,7 @@ const OnlineGameLink = ({
const exportPending = !errored && exportStep !== '' && exportStep !== 'done';
const isBuildComplete = build && build.status === 'complete';
const isBuildPublished = build && game && build.id === game.publicWebBuildId;
const gameUrl = getGameUrl(game);
const gameUrl = getGameUrl(game, slug);
const buildUrl =
exportPending || !isBuildComplete
? null
@@ -102,8 +106,16 @@ const OnlineGameLink = ({
const { id } = profile;
try {
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);
if (slugs && slugs.length > 0) {
setSlug(slugs[0]);
}
} catch (err) {
console.error('Unable to load the game', err);
} finally {
@@ -189,6 +201,35 @@ const OnlineGameLink = ({
}
);
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) {
showErrorBox({
message: i18n._(
@@ -377,6 +418,7 @@ const OnlineGameLink = ({
}
}}
game={game}
slug={slug}
isLoading={isGameLoading}
/>
)}

View File

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

View File

@@ -17,6 +17,7 @@ import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
import { I18n } from '@lingui/react';
import { Column, Line, Spacer } from '../UI/Grid';
import BackgroundText from '../UI/BackgroundText';
import AlertMessage from '../UI/AlertMessage';
import { GameThumbnail } from './GameThumbnail';
const isCyrillic = (text: string) =>
@@ -195,6 +196,7 @@ export function PublicGameProperties({
rows={5}
/>
{setUserSlug && setGameSlug && (
<>
<Line>
<SelectField
fullWidth
@@ -223,14 +225,39 @@ export function PublicGameProperties({
</SelectField>
<Spacer />
<SemiControlledTextField
disabled={
!(
userSlug &&
userSlug.length &&
profile &&
profile.username
)
}
floatingLabelText={<Trans>Game name in the game URL</Trans>}
fullWidth
type="text"
value={gameSlug || ''}
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
userIds={authorIds}

View File

@@ -16,6 +16,10 @@ export const GDevelopGamesPlatform = {
isDev
? `https://liluo.io/games/${gameId}?dev=true`
: `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) =>
username
? `https://liluo.io/${username}${isDev ? '?dev=true' : ''}`

View File

@@ -38,6 +38,12 @@ export type Game = {
discoverable?: boolean,
};
export type GameSlug = {
username: string,
gameSlug: string,
createdAt: number,
};
export type ShowcasedGameLink = {
url: string,
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;
return GDevelopGamesPlatform.getGameUrl(game.id);
return slug
? GDevelopGamesPlatform.getGameUrlWithSlug(slug.username, slug.gameSlug)
: GDevelopGamesPlatform.getGameUrl(game.id);
};
export const getAclsFromUserIds = (
@@ -374,3 +382,23 @@ export const getPublicGame = (gameId: string): Promise<PublicGame> => {
.get(`${GDevelopGameApi.baseUrl}/public-game/${gameId}`)
.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);
};