Skip to content

Commit

Permalink
Add possibility to display message in the preview when an in-app tuto…
Browse files Browse the repository at this point in the history
…rial is running (#7379)
  • Loading branch information
AlexandreSi authored Feb 10, 2025
1 parent 47d0fba commit 90cdc87
Show file tree
Hide file tree
Showing 14 changed files with 337 additions and 40 deletions.
1 change: 1 addition & 0 deletions GDJS/GDJS/IDE/Exporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
/*includeWindowMessageDebuggerClient=*/false,
/*includeMinimalDebuggerClient=*/false,
/*includeCaptureManager*/ false,
/*includeInAppTutorialMessage*/ false,
exportedProject.GetLoadingScreen().GetGDevelopLogoStyle(),
includesFiles);

Expand Down
13 changes: 13 additions & 0 deletions GDJS/GDJS/IDE/ExporterHelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ bool ExporterHelper::ExportProjectForPixiPreview(
options.useMinimalDebuggerClient,
/*includeCaptureManager=*/
!options.captureOptions.IsEmpty(),
/*includeInAppTutorialMessage*/
!options.inAppTutorialMessageInPreview.empty(),
immutableProject.GetLoadingScreen().GetGDevelopLogoStyle(),
includesFiles);

Expand Down Expand Up @@ -248,6 +250,12 @@ bool ExporterHelper::ExportProjectForPixiPreview(
runtimeGameOptions.AddChild("playerToken")
.SetStringValue(options.playerToken);
}
if (!options.inAppTutorialMessageInPreview.empty()) {
runtimeGameOptions.AddChild("inAppTutorialMessageInPreview")
.SetStringValue(options.inAppTutorialMessageInPreview);
runtimeGameOptions.AddChild("inAppTutorialMessagePositionInPreview")
.SetStringValue(options.inAppTutorialMessagePositionInPreview);
}
if (!options.crashReportUploadLevel.empty()) {
runtimeGameOptions.AddChild("crashReportUploadLevel")
.SetStringValue(options.crashReportUploadLevel);
Expand Down Expand Up @@ -797,6 +805,7 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
bool includeWindowMessageDebuggerClient,
bool includeMinimalDebuggerClient,
bool includeCaptureManager,
bool includeInAppTutorialMessage,
gd::String gdevelopLogoStyle,
std::vector<gd::String> &includesFiles) {
// First, do not forget common includes (they must be included before events
Expand Down Expand Up @@ -859,6 +868,10 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
InsertUnique(includesFiles, "splash/gd-logo-light.js");
}

if (includeInAppTutorialMessage) {
InsertUnique(includesFiles, "InAppTutorialMessage.js");
}

if (includeWebsocketDebuggerClient || includeWindowMessageDebuggerClient) {
InsertUnique(includesFiles, "debugger-client/hot-reloader.js");
InsertUnique(includesFiles, "debugger-client/abstract-debugger-client.js");
Expand Down
28 changes: 22 additions & 6 deletions GDJS/GDJS/IDE/ExporterHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ struct PreviewExportOptions {
fullLoadingScreen(false),
isDevelopmentEnvironment(false),
nonRuntimeScriptsCacheBurst(0),
inAppTutorialMessageInPreview(""),
inAppTutorialMessagePositionInPreview(""),
fallbackAuthorId(""),
fallbackAuthorUsername(""),
playerId(""),
Expand All @@ -64,6 +66,17 @@ struct PreviewExportOptions {
return *this;
}

/**
* \brief Set the message to display to the user in a preview (as part
* of an in-app tutorial).
*/
PreviewExportOptions &SetInAppTutorialMessageInPreview(
const gd::String &message, const gd::String &position) {
inAppTutorialMessageInPreview = message;
inAppTutorialMessagePositionInPreview = position;
return *this;
}

/**
* \brief Set the fallback author info (if info not present in project
* properties).
Expand Down Expand Up @@ -286,6 +299,8 @@ struct PreviewExportOptions {
gd::String playerId;
gd::String playerUsername;
gd::String playerToken;
gd::String inAppTutorialMessageInPreview;
gd::String inAppTutorialMessagePositionInPreview;
bool nativeMobileApp;
std::map<gd::String, int> includeFileHashes;
bool projectDataOnlyExport;
Expand Down Expand Up @@ -405,6 +420,7 @@ class ExporterHelper {
bool includeWindowMessageDebuggerClient,
bool includeMinimalDebuggerClient,
bool includeCaptureManager,
bool includeInAppTutorialMessage,
gd::String gdevelopLogoStyle,
std::vector<gd::String> &includesFiles);

Expand Down Expand Up @@ -468,12 +484,12 @@ class ExporterHelper {
* gdjs.RuntimeGame object.
*/
bool ExportIndexFile(const gd::Project &project,
gd::String source,
gd::String exportDir,
const std::vector<gd::String> &includesFiles,
const std::vector<gd::SourceFileMetadata> &sourceFiles,
unsigned int nonRuntimeScriptsCacheBurst,
gd::String additionalSpec = "");
gd::String source,
gd::String exportDir,
const std::vector<gd::String> &includesFiles,
const std::vector<gd::SourceFileMetadata> &sourceFiles,
unsigned int nonRuntimeScriptsCacheBurst,
gd::String additionalSpec = "");

/**
* \brief Replace the annotations in a index.html file by the specified
Expand Down
186 changes: 186 additions & 0 deletions GDJS/Runtime/InAppTutorialMessage.tsx

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions GDJS/Runtime/runtimegame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ namespace gdjs {
/** Any capture that should be done during the preview. */
captureOptions?: CaptureOptions;

/** Message to display to the user during an in-app tutorial. */
inAppTutorialMessageInPreview?: string;
inAppTutorialMessagePositionInPreview?: string;

/**
* If set, this data is used to authenticate automatically when launching the game.
* This is only useful during previews.
Expand Down Expand Up @@ -916,6 +920,17 @@ namespace gdjs {

this._setupGameVisibilityEvents();

if (
this._options.inAppTutorialMessageInPreview &&
gdjs.inAppTutorialMessage
) {
gdjs.inAppTutorialMessage.displayInAppTutorialMessage(
this,
this._options.inAppTutorialMessageInPreview,
this._options.inAppTutorialMessagePositionInPreview || ''
);
}

// The standard game loop
let accumulatedElapsedTime = 0;
this._hasJustResumed = false;
Expand Down
1 change: 1 addition & 0 deletions GDevelop.js/Bindings/Bindings.idl
Original file line number Diff line number Diff line change
Expand Up @@ -3887,6 +3887,7 @@ interface PreviewExportOptions {
[Ref] PreviewExportOptions UseWebsocketDebuggerClientWithServerAddress([Const] DOMString address, [Const] DOMString port);
[Ref] PreviewExportOptions UseWindowMessageDebuggerClient();
[Ref] PreviewExportOptions UseMinimalDebuggerClient();
[Ref] PreviewExportOptions SetInAppTutorialMessageInPreview([Const] DOMString message, [Const] DOMString position);
[Ref] PreviewExportOptions SetLayoutName([Const] DOMString layoutName);
[Ref] PreviewExportOptions SetFallbackAuthor([Const] DOMString id, [Const] DOMString username);
[Ref] PreviewExportOptions SetAuthenticatedPlayer([Const] DOMString playerId, [Const] DOMString playerUsername, [Const] DOMString playerToken);
Expand Down
1 change: 1 addition & 0 deletions GDevelop.js/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2869,6 +2869,7 @@ export class PreviewExportOptions extends EmscriptenObject {
useWebsocketDebuggerClientWithServerAddress(address: string, port: string): PreviewExportOptions;
useWindowMessageDebuggerClient(): PreviewExportOptions;
useMinimalDebuggerClient(): PreviewExportOptions;
setInAppTutorialMessageInPreview(message: string, position: string): PreviewExportOptions;
setLayoutName(layoutName: string): PreviewExportOptions;
setFallbackAuthor(id: string, username: string): PreviewExportOptions;
setAuthenticatedPlayer(playerId: string, playerUsername: string, playerToken: string): PreviewExportOptions;
Expand Down
1 change: 1 addition & 0 deletions GDevelop.js/types/gdpreviewexportoptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ declare class gdPreviewExportOptions {
useWebsocketDebuggerClientWithServerAddress(address: string, port: string): gdPreviewExportOptions;
useWindowMessageDebuggerClient(): gdPreviewExportOptions;
useMinimalDebuggerClient(): gdPreviewExportOptions;
setInAppTutorialMessageInPreview(message: string, position: string): gdPreviewExportOptions;
setLayoutName(layoutName: string): gdPreviewExportOptions;
setFallbackAuthor(id: string, username: string): gdPreviewExportOptions;
setAuthenticatedPlayer(playerId: string, playerUsername: string, playerToken: string): gdPreviewExportOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,13 @@ export default class BrowserS3PreviewLauncher extends React.Component<
previewExportOptions.setProjectTemplateSlug(project.getTemplateSlug());
previewExportOptions.setSourceGameId(this.props.sourceGameId);

if (previewOptions.inAppTutorialMessageInPreview) {
previewExportOptions.setInAppTutorialMessageInPreview(
previewOptions.inAppTutorialMessageInPreview,
previewOptions.inAppTutorialMessagePositionInPreview
);
}

if (previewOptions.fallbackAuthor) {
previewExportOptions.setFallbackAuthor(
previewOptions.fallbackAuthor.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,13 @@ export default class LocalPreviewLauncher extends React.Component<
);
previewExportOptions.setSourceGameId(this.props.sourceGameId);

if (previewOptions.inAppTutorialMessageInPreview) {
previewExportOptions.setInAppTutorialMessageInPreview(
previewOptions.inAppTutorialMessageInPreview,
previewOptions.inAppTutorialMessagePositionInPreview
);
}

if (previewOptions.fallbackAuthor) {
previewExportOptions.setFallbackAuthor(
previewOptions.fallbackAuthor.id,
Expand Down
2 changes: 2 additions & 0 deletions newIDE/app/src/ExportAndShare/PreviewLauncher.flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export type PreviewOptions = {|
getIsAlwaysOnTopInPreview: () => boolean,
captureOptions: CaptureOptions,
onCaptureFinished: CaptureOptions => Promise<void>,
inAppTutorialMessageInPreview: string,
inAppTutorialMessagePositionInPreview: string,
|};

/** The props that PreviewLauncher must support */
Expand Down
91 changes: 59 additions & 32 deletions newIDE/app/src/InAppTutorial/InAppTutorialOrchestrator.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// @flow
import * as React from 'react';
import { I18n } from '@lingui/react';
import { I18n as I18nType } from '@lingui/core';
import { useDebounce } from '../Utils/UseDebounce';
import { useInterval } from '../Utils/UseInterval';
Expand Down Expand Up @@ -428,6 +427,7 @@ type Props = {|
tutorial: InAppTutorial,
startStepIndex: number,
startProjectData: { [key: string]: string },
i18n: I18nType,
endTutorial: ({|
shouldCloseProject: boolean,
shouldWarnAboutUnsavedChanges: boolean,
Expand All @@ -445,6 +445,7 @@ export type InAppTutorialOrchestratorInterface = {|
projectData: {| [key: string]: string |},
|},
changeData: (oldName: string, newName: string) => void,
getPreviewMessage: () => {| message: string, position: string |} | null,
|};

const InAppTutorialOrchestrator = React.forwardRef<
Expand All @@ -460,6 +461,7 @@ const InAppTutorialOrchestrator = React.forwardRef<
currentSceneName,
startStepIndex,
startProjectData,
i18n,
},
ref
) => {
Expand Down Expand Up @@ -764,10 +766,39 @@ const InAppTutorialOrchestrator = React.forwardRef<
}
};

const getPreviewMessage = (): {|
message: string,
position: string,
|} | null => {
const { nextStepTrigger } = currentStep;
if (!nextStepTrigger || !nextStepTrigger.previewLaunched) return null;

const messageToUse = isTouchScreen
? nextStepTrigger.inGameTouchMessage || nextStepTrigger.inGameMessage
: nextStepTrigger.inGameMessage;

const message = messageToUse
? translateAndInterpolateText({
text: messageToUse,
data,
i18n,
project,
}) || null
: null;
if (message) {
return {
message,
position: nextStepTrigger.inGameMessagePosition || 'bottom-left',
};
}
return null;
};

React.useImperativeHandle(ref, () => ({
onPreviewLaunch,
getProgress,
changeData,
getPreviewMessage,
}));

const onPreviewLaunch = React.useCallback(
Expand Down Expand Up @@ -1023,7 +1054,7 @@ const InAppTutorialOrchestrator = React.forwardRef<

const isTouchScreen = useScreenType() === 'touch';

const renderStepDisplayer = (i18n: I18nType) => {
const renderStepDisplayer = () => {
if (!currentStep) return null;
const stepTooltip = currentStep.tooltip;
let formattedTooltip;
Expand Down Expand Up @@ -1142,37 +1173,33 @@ const InAppTutorialOrchestrator = React.forwardRef<
});

return (
<I18n>
{({ i18n }) => (
<>
{renderStepDisplayer(i18n)}
{displayEndDialog && (
<InAppTutorialDialog
isLastStep
dialogContent={endDialog}
endTutorial={() => {
setDisplayEndDialog(false);
if (isRunningMiniTutorial) {
// If running a mini tutorial, we save the progress to indicate that the user has finished this lesson.
preferences.saveTutorialProgress({
tutorialId: tutorial.id,
userId: authenticatedUser.profile
? authenticatedUser.profile.id
: undefined,
...getProgress(),
// We do not specify a storage provider, as we don't need to reload the project.
});
}
endTutorial({
shouldCloseProject: false,
shouldWarnAboutUnsavedChanges: !isRunningMiniTutorial,
});
}}
/>
)}
</>
<>
{renderStepDisplayer()}
{displayEndDialog && (
<InAppTutorialDialog
isLastStep
dialogContent={endDialog}
endTutorial={() => {
setDisplayEndDialog(false);
if (isRunningMiniTutorial) {
// If running a mini tutorial, we save the progress to indicate that the user has finished this lesson.
preferences.saveTutorialProgress({
tutorialId: tutorial.id,
userId: authenticatedUser.profile
? authenticatedUser.profile.id
: undefined,
...getProgress(),
// We do not specify a storage provider, as we don't need to reload the project.
});
}
endTutorial({
shouldCloseProject: false,
shouldWarnAboutUnsavedChanges: !isRunningMiniTutorial,
});
}}
/>
)}
</I18n>
</>
);
}
);
Expand Down
10 changes: 10 additions & 0 deletions newIDE/app/src/MainFrame/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1651,6 +1651,12 @@ const MainFrame = (props: Props) => {
await eventsFunctionsExtensionsState.ensureLoadFinished();

const startTime = Date.now();
let inAppTutorialMessageInPreview = { message: '', position: '' };
if (inAppTutorialOrchestratorRef.current) {
inAppTutorialMessageInPreview =
inAppTutorialOrchestratorRef.current.getPreviewMessage() ||
inAppTutorialMessageInPreview;
}
await previewLauncher.launchPreview({
project: currentProject,
layout,
Expand All @@ -1664,6 +1670,9 @@ const MainFrame = (props: Props) => {
getIsMenuBarHiddenInPreview: preferences.getIsMenuBarHiddenInPreview,
getIsAlwaysOnTopInPreview: preferences.getIsAlwaysOnTopInPreview,
numberOfWindows: numberOfWindows || 1,
inAppTutorialMessageInPreview: inAppTutorialMessageInPreview.message,
inAppTutorialMessagePositionInPreview:
inAppTutorialMessageInPreview.position,
captureOptions,
onCaptureFinished,
});
Expand Down Expand Up @@ -4011,6 +4020,7 @@ const MainFrame = (props: Props) => {
startStepIndex={startStepIndex}
startProjectData={startProjectData}
project={currentProject}
i18n={props.i18n}
endTutorial={({
shouldCloseProject,
shouldWarnAboutUnsavedChanges,
Expand Down
Loading

0 comments on commit 90cdc87

Please sign in to comment.