diff --git a/pxtservices/iframeDriver.ts b/pxtservices/iframeDriver.ts
new file mode 100644
index 000000000000..46f8447c3b69
--- /dev/null
+++ b/pxtservices/iframeDriver.ts
@@ -0,0 +1,553 @@
+///
+
+type MessageHandler = (response: any) => void;
+
+const MessageReceivedEvent = "message";
+const MessageSentEvent = "sent";
+
+interface PendingMessage {
+ original: pxt.editor.EditorMessageRequest;
+ resolve: MessageHandler;
+ reject: MessageHandler;
+}
+
+export interface IframeWorkspaceStatus {
+ projects: pxt.workspace.Project[];
+ editor?: pxt.editor.EditorSyncState;
+ controllerId?: string;
+}
+
+export interface IFrameWorkspaceHost {
+ saveProject(project: pxt.workspace.Project): Promise;
+ getWorkspaceProjects(): Promise;
+ resetWorkspace(): Promise;
+ onWorkspaceLoaded?(): Promise;
+}
+
+/**
+ * Manages communication with the editor iframe.
+ *
+ * TODO: Currently only supports a single iframe as all incoming messages
+ * are posted on the window. Would be nice in the future to refactor the
+ * iframe messages to use a MessageChannel.
+ */
+export class IframeDriver {
+ protected readyForMessages = false;
+ protected messageQueue: pxt.editor.EditorMessageRequest[] = [];
+ protected nextId = 0;
+ protected pendingMessages: { [index: string]: PendingMessage } = {};
+ protected editorEventListeners: { [index: string]: MessageHandler[] } = {};
+
+ constructor(public iframe: HTMLIFrameElement, public host?: IFrameWorkspaceHost) {
+ window.addEventListener("message", this.onMessageReceived);
+ }
+
+ dispose() {
+ window.removeEventListener("message", this.onMessageReceived);
+ }
+
+ async switchEditorLanguage(lang: "typescript" | "blocks" | "python") {
+ let action;
+ switch (lang) {
+ case "blocks":
+ action = "switchblocks";
+ break;
+ case "typescript":
+ action = "switchjavascript";
+ break;
+ case "python":
+ action = "switchpython";
+ break;
+ }
+
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action
+ } as pxt.editor.EditorMessageRequest
+ );
+ }
+
+ async setLanguageRestriction(restriction: pxt.editor.LanguageRestriction) {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "setlanguagerestriction",
+ restriction
+ } as pxt.editor.EditorSetLanguageRestriction
+ );
+ }
+
+ async startSimulator() {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "startsimulator"
+ } as pxt.editor.EditorMessageRequest
+ );
+ }
+
+ async stopSimulator(unload = false) {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "stopsimulator",
+ unload
+ } as pxt.editor.EditorMessageStopRequest
+ );
+ }
+
+ async restartSimulator() {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "restartsimulator"
+ } as pxt.editor.EditorMessageRequest
+ );
+ }
+
+ async hideSimulator() {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "hidesimulator"
+ } as pxt.editor.EditorMessageRequest
+ );
+ }
+
+ async showSimulator() {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "showsimulator"
+ } as pxt.editor.EditorMessageRequest
+ );
+ }
+
+ async setSimulatorFullscreen(on: boolean) {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "setsimulatorfullscreen",
+ enabled: on
+ } as pxt.editor.EditorMessageSetSimulatorFullScreenRequest
+ );
+ }
+
+ async closeFlyout() {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "closeflyout"
+ } as pxt.editor.EditorMessageRequest
+ );
+ }
+
+ async unloadProject() {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "unloadproject"
+ } as pxt.editor.EditorMessageRequest
+ );
+ }
+
+ async saveProject() {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "saveproject"
+ } as pxt.editor.EditorMessageRequest
+ );
+ }
+
+ async undo() {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "undo"
+ } as pxt.editor.EditorMessageRequest
+ );
+ }
+
+ async redo() {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "redo"
+ } as pxt.editor.EditorMessageRequest
+ );
+ }
+
+ async setHighContrast(on: boolean) {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "sethighcontrast",
+ on
+ } as pxt.editor.EditorMessageSetHighContrastRequest
+ );
+ }
+
+ async toggleHighContrast() {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "togglehighcontrast"
+ } as pxt.editor.EditorMessageRequest
+ );
+ }
+
+ async toggleGreenScreen() {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "togglegreenscreen"
+ } as pxt.editor.EditorMessageRequest
+ );
+ }
+
+ async toggleSloMo(intervalSpeed?: number) {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "toggletrace",
+ intervalSpeed
+ } as pxt.editor.EditorMessageToggleTraceRequest
+ );
+ }
+
+ async setSloMoEnabled(enabled: boolean, intervalSpeed?: number) {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "settracestate",
+ enabled,
+ intervalSpeed
+ } as pxt.editor.EditorMessageSetTraceStateRequest
+ );
+ }
+
+ async printProject() {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "print"
+ } as pxt.editor.EditorMessageRequest
+ );
+ }
+
+ async getInfo(): Promise {
+ const resp = await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "info"
+ } as pxt.editor.EditorMessageRequest
+ ) as pxt.editor.EditorMessageResponse;
+
+ return (resp.resp as pxt.editor.InfoMessage);
+ }
+
+ async newProject(options: pxt.editor.ProjectCreationOptions) {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "newproject",
+ options
+ } as pxt.editor.EditorMessageNewProjectRequest
+ );
+ }
+
+ async importProject(project: pxt.workspace.Project, filters?: pxt.editor.ProjectFilters, searchBar?: boolean) {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "importproject",
+ project,
+ filters,
+ searchBar
+ } as pxt.editor.EditorMessageImportProjectRequest
+ );
+ }
+
+ async openHeader(headerId: string) {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "openheader",
+ headerId
+ } as pxt.editor.EditorMessageOpenHeaderRequest
+ );
+ }
+
+ async shareHeader(headerId: string, projectName: string) {
+ const resp = await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "shareproject",
+ headerId,
+ projectName
+ } as pxt.editor.EditorShareRequest
+ ) as pxt.editor.EditorMessageResponse;
+
+ return resp.resp as pxt.editor.ShareData;
+ }
+
+ async startActivity(activityType: "tutorial" | "example" | "recipe", path: string, title?: string, previousProjectHeaderId?: string, carryoverPreviousCode?: boolean) {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "startactivity",
+ activityType,
+ path,
+ title,
+ previousProjectHeaderId,
+ carryoverPreviousCode
+ } as pxt.editor.EditorMessageStartActivity
+ );
+ }
+
+ async importTutorial(markdown: string) {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "importtutorial",
+ markdown
+ } as pxt.editor.EditorMessageImportTutorialRequest
+ );
+ }
+
+ async pair() {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "pair"
+ } as pxt.editor.EditorMessageRequest
+ );
+ }
+
+ async decompileToBlocks(ts: string, snippetMode?: boolean, layout?: pxt.editor.BlockLayout) {
+ const resp = await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "renderblocks",
+ ts,
+ snippetMode,
+ layout
+ } as pxt.editor.EditorMessageRenderBlocksRequest
+ ) as pxt.editor.EditorMessageResponse;
+
+ return resp.resp as pxt.editor.EditorMessageRenderBlocksResponse;
+ }
+
+ async decompileToPython(ts: string) {
+ const resp = await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "renderpython",
+ ts
+ } as pxt.editor.EditorMessageRenderPythonRequest
+ ) as pxt.editor.EditorMessageResponse;
+
+ return (resp.resp as pxt.editor.EditorMessageRenderPythonResponse).python;
+ }
+
+ async runValidatorPlan(validatorPlan: pxt.blocks.ValidatorPlan, planLib: pxt.blocks.ValidatorPlan[]) {
+ const resp = await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "runeval",
+ validatorPlan,
+ planLib,
+ } as pxt.editor.EditorMessageRunEvalRequest
+ ) as pxt.editor.EditorMessageResponse;
+
+ return resp.resp as pxt.blocks.EvaluationResult;
+ }
+
+ async saveLocalProjectsToCloud(headerIds: string[]) {
+ const resp = await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "savelocalprojectstocloud",
+ headerIds
+ } as pxt.editor.EditorMessageSaveLocalProjectsToCloud
+ ) as pxt.editor.EditorMessageResponse;
+
+ return resp.resp as pxt.editor.EditorMessageSaveLocalProjectsToCloudResponse;
+ }
+
+ async convertCloudProjectsToLocal(userId: string) {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "convertcloudprojectstolocal",
+ userId
+ } as pxt.editor.EditorMessageConvertCloudProjectsToLocal
+ );
+ }
+
+ async requestProjectCloudStatus(headerIds: string[]) {
+ await this.sendRequest(
+ {
+ type: "pxteditor",
+ action: "requestprojectcloudstatus",
+ headerIds
+ } as pxt.editor.EditorMessageRequestProjectCloudStatus
+ );
+ }
+
+ addEventListener(event: typeof MessageSentEvent, handler: (ev: pxt.editor.EditorMessage) => void): void;
+ addEventListener(event: typeof MessageReceivedEvent, handler: (ev: pxt.editor.EditorMessage) => void): void;
+ addEventListener(event: "event", handler: (ev: pxt.editor.EditorMessageEventRequest) => void): void;
+ addEventListener(event: "simevent", handler: (ev: pxt.editor.EditorSimulatorEvent) => void): void;
+ addEventListener(event: "tutorialevent", handler: (ev: pxt.editor.EditorMessageTutorialEventRequest) => void): void;
+ addEventListener(event: "editorcontentloaded", handler: (ev: pxt.editor.EditorContentLoadedRequest) => void): void;
+ addEventListener(event: "workspacesave", handler: (ev: pxt.editor.EditorWorkspaceSaveRequest) => void): void;
+ addEventListener(event: "workspaceevent", handler: (ev: pxt.editor.EditorWorkspaceEvent) => void): void;
+ addEventListener(event: "workspacereset", handler: (ev: pxt.editor.EditorWorkspaceSyncRequest) => void): void;
+ addEventListener(event: "workspacesync", handler: (ev: pxt.editor.EditorWorkspaceSyncRequest) => void): void;
+ addEventListener(event: "workspaceloaded", handler: (ev: pxt.editor.EditorWorkspaceSyncRequest) => void): void;
+ addEventListener(event: "workspacediagnostics", handler: (ev: pxt.editor.EditorWorkspaceDiagnostics) => void): void;
+ addEventListener(event: "editorcontentloaded", handler: (ev: pxt.editor.EditorContentLoadedRequest) => void): void;
+ addEventListener(event: "projectcloudstatus", handler: (ev: pxt.editor.EditorMessageProjectCloudStatus) => void): void;
+ addEventListener(event: string, handler: (ev: any) => void): void {
+ if (!this.editorEventListeners[event]) this.editorEventListeners[event] = [];
+ this.editorEventListeners[event].push(handler);
+ }
+
+ removeEventListener(event: string, handler: (ev: any) => void) {
+ if (this.editorEventListeners[event]) {
+ const filtered = this.editorEventListeners[event].filter(h => h !== handler);
+
+ if (filtered.length === 0) {
+ delete this.editorEventListeners[event];
+ }
+ else {
+ this.editorEventListeners[event] = filtered;
+ }
+ }
+ }
+
+ sendMessage(message: pxt.editor.EditorMessageRequest): Promise {
+ return this.sendRequest(message) as Promise;
+ }
+
+ protected onMessageReceived = (event: MessageEvent) => {
+ const data = event.data as pxt.editor.EditorMessageRequest;
+ if (!data || !/^pxt(host|editor|pkgext|sim)$/.test(data.type)) return;
+
+ if (data.type === "pxteditor") {
+ if (data.id && this.pendingMessages[data.id]) {
+ const resp = event.data as pxt.editor.EditorMessageResponse;
+ const pending = this.pendingMessages[resp.id!];
+ delete this.pendingMessages[resp.id!];
+
+ if (resp.success) {
+ pending.resolve(resp);
+ }
+ else {
+ pending.reject(resp.error || new Error("Unknown error: iFrame returned failure"));
+ }
+ }
+ }
+ else if (data.type === "pxthost") {
+ if (data.action === "editorcontentloaded") {
+ this.readyForMessages = true;
+ this.sendMessageCore(); // flush message queue.
+ }
+ else if (data.action === "workspacesync" || data.action === "workspacesave" || data.action === "workspacereset" || data.action === "workspaceloaded") {
+ this.handleWorkspaceSync(data as pxt.editor.EditorWorkspaceSyncRequest);
+ }
+
+ this.fireEvent(data.action, data);
+ }
+
+ this.fireEvent(MessageReceivedEvent, data);
+ }
+
+ protected fireEvent(event: string, data: any) {
+ const listeners = this.editorEventListeners[event];
+ if (!listeners) return;
+
+ for (const handler of listeners) {
+ try {
+ handler(data);
+ }
+ catch (e) {
+ console.error(e);
+ }
+ }
+ }
+
+ protected async handleWorkspaceSync(event: pxt.editor.EditorWorkspaceSyncRequest | pxt.editor.EditorWorkspaceSaveRequest) {
+ if (!this.host) return;
+
+ let error: any = undefined;
+ try {
+ if (event.action === "workspacesync") {
+ const status = await this.host.getWorkspaceProjects();
+ this.sendMessageCore({
+ type: "pxthost",
+ id: event.id,
+ success: !!status,
+ projects: status?.projects,
+ editor: status?.editor,
+ controllerId: status?.controllerId
+ } as pxt.editor.EditorWorkspaceSyncResponse);
+ }
+ else if (event.action === "workspacereset") {
+ await this.host.resetWorkspace();
+ }
+ else if (event.action === "workspacesave") {
+ await this.host.saveProject(event.project);
+ }
+ else if (event.action === "workspaceloaded") {
+ if (this.host.onWorkspaceLoaded) {
+ await this.host.onWorkspaceLoaded();
+ }
+ }
+ }
+ catch (e) {
+ error = e;
+ console.error(e);
+ }
+ finally {
+ if (event.response) {
+ this.sendMessageCore({
+ type: "pxthost",
+ id: event.id,
+ success: !error,
+ error
+ } as pxt.editor.EditorMessageResponse);
+ }
+ }
+ }
+
+ protected sendRequest(message: any) {
+ return new Promise((resolve, reject) => {
+ message.response = true;
+ message.id = this.nextId++ + "";
+ this.pendingMessages[message.id] = {
+ original: message,
+ resolve,
+ reject
+ };
+ this.sendMessageCore(message);
+ });
+ }
+
+ protected sendMessageCore(message?: any) {
+ if (message) {
+ this.messageQueue.push(message);
+ }
+
+ if (this.readyForMessages) {
+ while (this.messageQueue.length) {
+ const toSend = this.messageQueue.shift();
+ this.iframe.contentWindow?.postMessage(toSend, "*");
+ this.fireEvent(MessageSentEvent, toSend);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/pxtservices/tsconfig.json b/pxtservices/tsconfig.json
new file mode 100644
index 000000000000..12a27c367831
--- /dev/null
+++ b/pxtservices/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "compilerOptions": {
+ "target": "es2017",
+ "noImplicitAny": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "module": "commonjs",
+ "outDir": "../built/pxtservices",
+ "rootDir": ".",
+ "newLine": "LF",
+ "sourceMap": false,
+ "moduleResolution": "node",
+ "isolatedModules": false,
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true,
+ "declaration": true,
+ "preserveConstEnums": true,
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "scripthost",
+ "es2017",
+ "ES2018.Promise"
+ ],
+ "incremental": false,
+ "skipLibCheck": true
+ },
+ "exclude": [
+ "node_modules"
+ ]
+}
\ No newline at end of file
diff --git a/teachertool/src/components/MakecodeFrame.tsx b/teachertool/src/components/MakecodeFrame.tsx
index f5a65cdf345c..34c0c8be95f4 100644
--- a/teachertool/src/components/MakecodeFrame.tsx
+++ b/teachertool/src/components/MakecodeFrame.tsx
@@ -2,7 +2,7 @@
import css from "./styling/MakeCodeFrame.module.scss";
import { useContext, useEffect } from "react";
-import { clearReady, setEditorRef } from "../services/makecodeEditorService";
+import { setEditorRef } from "../services/makecodeEditorService";
import { AppStateContext } from "../state/appStateContext";
import { getEditorUrl } from "../utils";
@@ -13,7 +13,9 @@ export const MakeCodeFrame: React.FC = () => {
// Clear iframe state when the iframe url is changed
useEffect(() => {
- clearReady();
+ if (!teacherTool.projectMetadata?.id) {
+ setEditorRef(undefined);
+ }
}, [teacherTool.projectMetadata?.id]);
function createIFrameUrl(shareId: string): string {
@@ -30,7 +32,9 @@ export const MakeCodeFrame: React.FC = () => {
}
const handleIFrameRef = (el: HTMLIFrameElement | null) => {
- setEditorRef(el ?? undefined);
+ if (el) {
+ setEditorRef(el);
+ }
};
/* eslint-disable @microsoft/sdl/react-iframe-missing-sandbox */
diff --git a/teachertool/src/services/makecodeEditorService.ts b/teachertool/src/services/makecodeEditorService.ts
index 9df72cda030b..c66bf4d0082d 100644
--- a/teachertool/src/services/makecodeEditorService.ts
+++ b/teachertool/src/services/makecodeEditorService.ts
@@ -3,92 +3,44 @@
import { ErrorCode } from "../types/errorCode";
import { logDebug, logError } from "./loggingService";
import * as AutorunService from "./autorunService";
+import { IframeDriver } from "pxtservices/iframeDriver";
-interface PendingMessage {
- original: pxt.editor.EditorMessageRequest;
- handler: (response: any) => void;
-}
-
-let makecodeEditorRef: HTMLIFrameElement | undefined;
-let readyForMessages: boolean;
-const messageQueue: pxt.editor.EditorMessageRequest[] = [];
-let nextId: number = 0;
-let pendingMessages: { [index: string]: PendingMessage } = {};
-function onMessageReceived(event: MessageEvent) {
- logDebug(`Message received from iframe: ${JSON.stringify(event.data)}`);
+let driver: IframeDriver | undefined;
+let highContrast: boolean;
- const data = event.data as pxt.editor.EditorMessageRequest;
- if (data.type === "pxteditor") {
- if (data.action === "editorcontentloaded") {
- readyForMessages = true;
- sendMessageAsync(); // flush message queue.
- AutorunService.poke();
- }
-
- if (data.id && pendingMessages[data.id]) {
- const pending = pendingMessages[data.id];
- pending.handler(data);
- delete pendingMessages[data.id];
- }
- }
-}
-
-function sendMessageAsync(message?: any) {
- return new Promise(resolve => {
- const sendMessageCore = (message: any) => {
- logDebug(`Sending message to iframe: ${JSON.stringify(message)}`);
- makecodeEditorRef!.contentWindow!.postMessage(message, "*");
- };
-
- if (message) {
- message.response = true;
- message.id = nextId++ + "";
- pendingMessages[message.id] = {
- original: message,
- handler: resolve,
- };
- messageQueue.push(message);
- }
- if (readyForMessages) {
- while (messageQueue.length) {
- sendMessageCore(messageQueue.shift());
- }
- }
- });
-}
+export function setEditorRef(ref: HTMLIFrameElement | undefined) {
+ if (driver) {
+ if (driver.iframe === ref) return;
-// Check if the result was successful and (if expected) has data.
-// Logs errors and throws if the result was not successful.
-function validateResponse(result: pxt.editor.EditorMessageResponse, expectResponseData: boolean) {
- if (!result.success) {
- throw new Error(`Server returned failed status.`);
- }
- if (expectResponseData && !result?.resp) {
- throw new Error(`Missing response data.`);
+ driver.dispose();
+ driver = undefined;
}
-}
-export function clearReady() {
- readyForMessages = false;
-}
-
-export function setEditorRef(ref: HTMLIFrameElement | undefined) {
- makecodeEditorRef = ref ?? undefined;
- window.removeEventListener("message", onMessageReceived);
if (ref) {
- window.addEventListener("message", onMessageReceived);
+ driver = new IframeDriver(ref);
+
+ driver.addEventListener("message", ev => {
+ logDebug(`Message received from iframe: ${JSON.stringify(ev)}`);
+ });
+ driver.addEventListener("sent", ev => {
+ logDebug(`Sent message to iframe: ${JSON.stringify(ev)}`);
+ });
+ driver.addEventListener("editorcontentloaded", ev => {
+ AutorunService.poke();
+ });
+
+ driver.setHighContrast(highContrast);
}
}
// an example of events that we want to/can send to the editor
export async function setHighContrastAsync(on: boolean) {
- const result = await sendMessageAsync({
- type: "pxteditor",
- action: "sethighcontrast",
- on: on,
- });
- console.log(result);
+ highContrast = on;
+
+ if (driver) {
+ await driver!.setHighContrast(on)
+ }
}
export async function runValidatorPlanAsync(
@@ -98,15 +50,11 @@ export async function runValidatorPlanAsync(
let evalResults = undefined;
try {
- const response = await sendMessageAsync({
- type: "pxteditor",
- action: "runeval",
- validatorPlan: validatorPlan,
- planLib: planLib,
- } as pxt.editor.EditorMessageRunEvalRequest);
- const result = response as pxt.editor.EditorMessageResponse;
- validateResponse(result, true); // Throws on failure
- evalResults = result.resp as pxt.blocks.EvaluationResult;
+ evalResults = await driver!.runValidatorPlan(validatorPlan, planLib);
+
+ if (!evalResults) {
+ throw new Error(`Missing response data.`);
+ }
} catch (e: any) {
logError(ErrorCode.runEval, e);
}
diff --git a/teachertool/tsconfig.paths.json b/teachertool/tsconfig.paths.json
index ae46474ee0ea..91128d01bc3b 100644
--- a/teachertool/tsconfig.paths.json
+++ b/teachertool/tsconfig.paths.json
@@ -3,7 +3,8 @@
"paths": {
"react-common/*": ["../react-common/*"],
"react/*": ["../node_modules/react/*"],
- "react-dom/*": ["../node_modules/react-dom/*"]
+ "react-dom/*": ["../node_modules/react-dom/*"],
+ "pxtservices/*": ["../pxtservices/*"]
}
}
}
diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx
index 6f39d909e06b..8305bdfcc720 100644
--- a/webapp/src/app.tsx
+++ b/webapp/src/app.tsx
@@ -3801,12 +3801,20 @@ export class ProjectView
const cloudStatus = cloudMd?.cloudStatus();
if (cloudStatus) {
const msg: pxt.editor.EditorMessageProjectCloudStatus = {
- type: "pxteditor",
+ type: "pxthost",
action: "projectcloudstatus",
headerId: cloudMd.headerId,
status: cloudStatus.value
};
pxteditor.postHostMessageAsync(msg);
+
+ // Deprecated: This was originally fired with the "pxteditor"
+ // type, which should only be used for responses, not events.
+ // Use the pxthost version above instead
+ pxteditor.postHostMessageAsync({
+ ...msg,
+ type: "pxteditor"
+ });
}
}
@@ -4831,10 +4839,20 @@ export class ProjectView
}
}
- pxteditor.postHostMessageAsync({
- type: "pxteditor",
+ const msg: pxt.editor.EditorContentLoadedRequest = {
+ type: "pxthost",
action: "editorcontentloaded"
- } as pxt.editor.EditorContentLoadedRequest)
+ };
+
+ pxteditor.postHostMessageAsync(msg);
+
+ // Deprecated: This was originally fired with the "pxteditor"
+ // type, which should only be used for responses, not events.
+ // Use the pxthost version above instead
+ pxteditor.postHostMessageAsync({
+ ...msg,
+ type: "pxteditor"
+ });
if (this.pendingImport) {
this.pendingImport.resolve();
diff --git a/webapp/src/cloud.ts b/webapp/src/cloud.ts
index 378efe38ecda..4548c215b172 100644
--- a/webapp/src/cloud.ts
+++ b/webapp/src/cloud.ts
@@ -564,13 +564,22 @@ export async function requestProjectCloudStatus(headerIds: string[]): Promise