Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OCV first pass #10355

Merged
merged 34 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d31eca8
plumbing for showing the feedback iframe
srietkerk Dec 12, 2024
f55fca2
add https flag to the cli to use https when debugging ocv iframe
srietkerk Dec 19, 2024
c705f96
Merge branch 'master' of https://github.com/microsoft/pxt into srietk…
srietkerk Dec 20, 2024
87236db
more config, removed functions, changed height and width of iframe
srietkerk Dec 20, 2024
0593552
using different modal for the feedback
srietkerk Jan 14, 2025
5259143
added feedback menu item to editor menu
srietkerk Jan 14, 2025
9354dc2
got rid of iframe border
srietkerk Jan 14, 2025
a764d25
some small changes and styling
srietkerk Jan 25, 2025
6446b2a
added comments
srietkerk Jan 27, 2025
2120588
move app id to configs
srietkerk Jan 27, 2025
4da2fdc
fix onclose reference
srietkerk Jan 27, 2025
05d4be7
moved frame url to configs
srietkerk Jan 27, 2025
816c662
remove tutorial completion feedback for now
srietkerk Jan 27, 2025
8336f42
use emoji instead of sui icon
srietkerk Jan 27, 2025
bab52a2
merged master
srietkerk Jan 27, 2025
f905236
update iframeelement variable in event listener
srietkerk Jan 28, 2025
4a4eb79
add space to less file
srietkerk Jan 28, 2025
6c86476
another iframe element var change
srietkerk Jan 28, 2025
cd43ef9
moved height, width to less file, fixed some styling
srietkerk Jan 28, 2025
be206b5
remove redundant jsdoc types
srietkerk Jan 28, 2025
2bb7a05
use pxt log functions, fix spacing in the event listener
srietkerk Jan 28, 2025
b209592
move emoji out of lf
srietkerk Jan 28, 2025
9438c18
use lf for the displayed strings for the rating
srietkerk Jan 28, 2025
158939c
types added
srietkerk Jan 30, 2025
c714b42
move types to localtypings and wrap them in a namespace
srietkerk Jan 30, 2025
728ebd8
change to using the comment bubble icon
srietkerk Jan 30, 2025
69ec94d
added types
srietkerk Jan 31, 2025
9dc5367
actually using feedback kind
srietkerk Feb 4, 2025
3e8c6ba
rename feedback app theme field
srietkerk Feb 4, 2025
83b45f8
move iframe init and posting code to helper function
srietkerk Feb 4, 2025
980020a
use optional chaining for ondismiss call
srietkerk Feb 4, 2025
3b6a3ff
move current theme into send update theme function, update theme options
srietkerk Feb 4, 2025
943c493
move questions for rating config to a function
srietkerk Feb 4, 2025
36c823c
move appid, frame url to apptheme
srietkerk Feb 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2805,7 +2805,6 @@ function renderDocs(builtPackaged: string, localDir: string) {
}
pxt.log(`All docs written.`);
}

export function serveAsync(parsed: commandParser.ParsedCommand) {
// always use a cloud build
// in most cases, the user machine is not properly setup to
Expand Down Expand Up @@ -2864,6 +2863,7 @@ export function serveAsync(parsed: commandParser.ParsedCommand) {
serial: !parsed.flags["noSerial"] && !globalConfig.noSerial,
noauth: parsed.flags["noauth"] as boolean || false,
backport: parsed.flags["backport"] as number || 0,
https: parsed.flags["https"] as boolean || false,
}))
}

Expand Down Expand Up @@ -7129,7 +7129,8 @@ ${pxt.crowdin.KEY_VARIABLE} - crowdin key
description: "port where the locally running backend is listening.",
argument: "backport",
type: "number",
}
},
https: { description: "use https protocol instead of http"}
}
}, serveAsync);

Expand Down
19 changes: 14 additions & 5 deletions cli/server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as fs from 'fs';
import * as path from 'path';
import * as http from 'http';
import * as https from 'https';
import * as url from 'url';
import * as querystring from 'querystring';
import * as nodeutil from './nodeutil';
Expand All @@ -13,6 +14,7 @@ import { promisify } from "util";

import U = pxt.Util;
import Cloud = pxt.Cloud;
import { SecureContextOptions } from 'tls';

const userProjectsDirName = "projects";

Expand Down Expand Up @@ -804,6 +806,7 @@ export interface ServeOptions {
serial?: boolean;
noauth?: boolean;
backport?: number;
https?: boolean;
}

// can use http://localhost:3232/streams/nnngzlzxslfu for testing
Expand Down Expand Up @@ -949,8 +952,7 @@ export function serveAsync(options: ServeOptions) {
const wsServerPromise = initSocketServer(serveOptions.wsPort, serveOptions.hostname);
if (serveOptions.serial)
initSerialMonitor();

const server = http.createServer(async (req, res) => {
const reqListener: http.RequestListener = async (req, res) => {
const error = (code: number, msg: string = null) => {
res.writeHead(code, { "Content-Type": "text/plain" })
res.end(msg || "Error " + code)
Expand Down Expand Up @@ -1351,7 +1353,10 @@ export function serveAsync(options: ServeOptions) {
});
}
return
});
};
const canUseHttps = serveOptions.https && process.env["HTTPS_KEY"] && process.env["HTTPS_CERT"];
const httpsServerOptions: SecureContextOptions = {cert: process.env["HTTPS_CERT"], key: process.env["HTTPS_KEY"]};
const server = canUseHttps ? https.createServer(httpsServerOptions, reqListener) : http.createServer(reqListener);

// if user has a server.js file, require it
const serverjs = path.resolve(path.join(root, 'built', 'server.js'))
Expand All @@ -1362,12 +1367,16 @@ export function serveAsync(options: ServeOptions) {

const serverPromise = new Promise<void>((resolve, reject) => {
server.on("error", reject);
server.listen(serveOptions.port, serveOptions.hostname, () => resolve());
server.listen(serveOptions.port, serveOptions.hostname, () => {
console.log(`Server listening on port ${serveOptions.port}`);
return resolve()
});
});

return Promise.all([wsServerPromise, serverPromise])
.then(() => {
const start = `http://${serveOptions.hostname}:${serveOptions.port}/#local_token=${options.localToken}&wsport=${serveOptions.wsPort}`;
const protocol = canUseHttps ? "https" : "http";
const start = `${protocol}://${serveOptions.hostname}:${serveOptions.port}/#local_token=${options.localToken}&wsport=${serveOptions.wsPort}`;
console.log(`---------------------------------------------`);
console.log(``);
console.log(`To launch the editor, open this URL:`);
Expand Down
1 change: 1 addition & 0 deletions localtypings/pxtarget.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ declare namespace pxt {
timeMachineDiffInterval?: number; // An interval in milliseconds at which to take diffs to store in project history. Defaults to 5 minutes
timeMachineSnapshotInterval?: number; // An interval in milliseconds at which to take full project snapshots in project history. Defaults to 15 minutes
adjustBlockContrast?: boolean; // If set to true, all block colors will automatically be adjusted to have a contrast ratio of 4.5 with text
giveFeedback?: boolean; // Show the give feedback button in the settings menu
srietkerk marked this conversation as resolved.
Show resolved Hide resolved
}

interface DownloadDialogTheme {
Expand Down
2 changes: 2 additions & 0 deletions localtypings/pxteditor.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,7 @@ declare namespace pxt.editor {
extensionsVisible?: boolean;
isMultiplayerGame?: boolean; // Arcade: Does the current project contain multiplayer blocks?
onboarding?: pxt.tour.BubbleStep[];
feedback?: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this field mean? Can it be better named? Or add a comment after it?

}

export interface EditorState {
Expand Down Expand Up @@ -1047,6 +1048,7 @@ declare namespace pxt.editor {
showLanguagePicker(): void;
showShareDialog(title?: string, kind?: "multiplayer" | "vscode" | "share"): void;
showAboutDialog(): void;
showFeedbackDialog(): void;
showTurnBackTimeDialogAsync(): Promise<void>;

showLoginDialog(continuationHash?: string): void;
Expand Down
71 changes: 71 additions & 0 deletions react-common/components/controls/Feedback/Feedback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useEffect } from "react"
import { initFeedbackEventListener, removeFeedbackEventListener } from "./FeedbackEventListener";
import { baseConfig, ratingFeedbackConfig, appId, feedbackFrameUrl } from "./configs";
import { Modal } from "../Modal";

// both components require onClose because the feedback modal should close when the user clicks the "finish" button
// this would not happen if the EventListener did not have a callback to close the modal
interface IFeedbackModalProps {
feedbackConfig: any;
frameId: string;
title: string;
onClose: () => void;
}

// right now, there are two kinds of feedback that I think could be valuable for our targets
// generic and rating feedback, but we will likely want to expand this
interface IFeedbackProps {
kind: "generic" | "rating";
onClose: () => void;
}

// Wrapper component of the feedback modal so kind can determine what feedback actually shows in the modal
export const Feedback = (props: IFeedbackProps) => {
const { kind, onClose } = props;
return (
<>
{kind === "generic" &&
<FeedbackModal
feedbackConfig={baseConfig}
frameId="menu-feedback-frame"
title={lf("Leave Feedback")}
onClose={onClose}
/>}
{kind === "rating" &&
<FeedbackModal
feedbackConfig={ratingFeedbackConfig}
frameId="activity-feedback-frame"
title={lf("Rate this activity")}
onClose={onClose} />
}
</>
)
}

export const FeedbackModal = (props: IFeedbackModalProps) => {
const { feedbackConfig, frameId, title, onClose } = props;

const onDismiss = () => {
onClose();
}

let callbacks = { onDismiss };

useEffect(() => {
initFeedbackEventListener(feedbackConfig, frameId, callbacks);
return () => {
removeFeedbackEventListener();
}
}, [])
return (
<Modal className="feedback-modal" title={title} onClose={onClose}>
<iframe
title="feedback"
height="450px"
width="550px"
srietkerk marked this conversation as resolved.
Show resolved Hide resolved
id={frameId}
src={`${feedbackFrameUrl}/centrohost?appname=ocvfeedback&feature=host-ocv-inapp-feedback&platform=web&appId=${appId}#/hostedpage`}
sandbox="allow-scripts allow-same-origin allow-forms allow-popups" />
</Modal>
)
}
125 changes: 125 additions & 0 deletions react-common/components/controls/Feedback/FeedbackEventListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { appId, feedbackFrameUrl } from './configs';
interface FeedbackRequestEventPayload<T> {
Event: string
EventArgs: string
}
type FeedbackRequestPayloadType = FeedbackRequestEventPayload<any>
srietkerk marked this conversation as resolved.
Show resolved Hide resolved

interface FeedbackResponseEventPayload<T> {
event: string
data: T
error?: any
}

// for styling the feedback, we use this object. It is mostly used to change the colors.
// we'll want to change this based on the target and whether high contrast is enabled
let themeOptions = {
baseTheme: "PublisherLightTheme",
}

let initfeedbackOptions: any;
let feedbackData: any;
let FEEDBACK_FRAME_ID: string;
let currentTheme = '';
let feedbackCallbacks: any;

// the function to initialize the feedback event listener
// feedbackConfig: needs to be passed in as a prop because the things that
/**
* The function to initialize the feedback event listener
* @param {any} feedbackConfig: the feedback config object whose fields are defined in OCV.
srietkerk marked this conversation as resolved.
Show resolved Hide resolved
* This changes based on what type of feedback we want to collect. Look at configs.ts for more details.
* @param {string} frameId: the html id of the actual iframe where the feedback will be displayed
* @param {any} [callbacks]: an object of functions that can be called when certain events happen in the feedback modal.
* Needs to be passed in because the callbacks will depend on what the parent wants to react to.
*/
export const initFeedbackEventListener = (feedbackConfig: any, frameId: string, callbacks?: any) => {
srietkerk marked this conversation as resolved.
Show resolved Hide resolved
window.addEventListener('message', feedbackCallbackEventListener);
feedbackCallbacks = callbacks;
initfeedbackOptions = {
appId: appId,
ageGroup: "Undefined",
authenticationType: "Unauthenticated",
clientName: "MakeCode",
feedbackConfig: feedbackConfig,
isProduction: false,
themeOptions: themeOptions,
// telemetry - will likely want this
}

feedbackData = initfeedbackOptions;
FEEDBACK_FRAME_ID = frameId;
}

export const removeFeedbackEventListener = () => {
window.removeEventListener('message', feedbackCallbackEventListener);
}

/**
* The function that listens for the feedback events.
* The events here are the ones that seemed most useful to log or respond to
* @param {MessageEvent<FeedbackRequestPayloadType>} event: the event received from OCV
*/
const feedbackCallbackEventListener = (event: MessageEvent<FeedbackRequestPayloadType>) => {
srietkerk marked this conversation as resolved.
Show resolved Hide resolved
if (event.data.Event) {
const payload: FeedbackRequestPayloadType = event.data
switch (payload.Event) {
case 'InAppFeedbackInitOptions': //This is required to initialise feedback
sendFeedbackInitOptions()
break
case 'InAppFeedbackOnError': //Invoked when an error occurrs on feedback submission - would be nice to log something to the user
console.log('Error Message: ', payload.EventArgs)
srietkerk marked this conversation as resolved.
Show resolved Hide resolved
break
case 'InAppFeedbackInitializationComplete': //Invoked when feedback form is fully initialised and displays error/warning if any - nice to have a log for this
console.log('InAppFeedbackInitializationComplete: ', payload.EventArgs)
srietkerk marked this conversation as resolved.
Show resolved Hide resolved
break
case 'InAppFeedbackOnSuccess': //Invoked when feedback submission is successful - would be useful to have telemetry/something else on this event
console.log('InAppFeedbackOnSuccess: ', payload.EventArgs)
break
case 'InAppFeedbackDismissWithResult': //Invoked when feedback is dismissed - the big important one for us to be able to close the feedback modal
console.log('InAppFeedbackDismissWithResult: ', payload.EventArgs);
if (feedbackCallbacks.onDismiss) {
feedbackCallbacks.onDismiss();
}
break
}
}
}

// ***************** Helper Functions *****************

// TODO
// haven't implemented yet with events, but this will be needed in order to update to high contrast
// general changes need to be made as well use the correct theme. the windows ones were just the defaults.
const sendUpdateTheme = () => {
srietkerk marked this conversation as resolved.
Show resolved Hide resolved
type FeedbackResponsePayloadType = FeedbackResponseEventPayload<any>
if (currentTheme == 'WindowsDark') {
currentTheme = 'WindowsLight'
} else {
currentTheme = 'WindowsDark'
}
const response: FeedbackResponsePayloadType = {
event: 'OnFeedbackHostAppThemeChanged',
data: {
baseTheme: currentTheme,
},
}
const iFrameEle = document.getElementById(FEEDBACK_FRAME_ID) as HTMLIFrameElement
iFrameEle!.contentWindow!.postMessage(response, feedbackFrameUrl)
srietkerk marked this conversation as resolved.
Show resolved Hide resolved
}


/**
* Actually initializes the feedback session. This is called when the feedback modal opens.
*/
const sendFeedbackInitOptions = () => {
type FeedbackResponsePayloadType = FeedbackResponseEventPayload<any>
feedbackData.callbackFunctions = undefined
let response: FeedbackResponsePayloadType = {
event: 'InAppFeedbackInitOptions',
data: feedbackData,
}
response = JSON.parse(JSON.stringify(response))
srietkerk marked this conversation as resolved.
Show resolved Hide resolved
const iFrameEle = document.getElementById(FEEDBACK_FRAME_ID) as HTMLIFrameElement
iFrameEle!.contentWindow!.postMessage(response, feedbackFrameUrl)
srietkerk marked this conversation as resolved.
Show resolved Hide resolved
}
57 changes: 57 additions & 0 deletions react-common/components/controls/Feedback/configs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
export const appId = 50315;
export const feedbackFrameUrl = 'https://admin-ignite.microsoft.com';
Copy link
Contributor

@thsparks thsparks Jan 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are feedbackFrameUrl and appId things we should store in targetconfig?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was definitely something I was considering, but these are going to be the same for each of our targets, so it feels redundant to put these in the target config of each target.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose that's true. Ideally, I think we'd want to put this somewhere that wouldn't require a release to change (i.e. what happened with our CDN) but I'm not sure if we have a good option for that without going through our backend.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this url should go in pxtarget.json, even if it is the same for each target. This is what we do for privacyUrl, termsOfUseUrl, etc.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somewhat related question: is the OCV/feedback feature only enabled for 1st party targets?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we should only be showing feedback in our self-hosted targets. There is more work to really verify this, which will be my next priority, but for now, it's just something that gets set in the apptheme as something to include in the target.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were you able to move these hard-coded config values to pxtarget.json?


export const baseConfig: any = {
feedbackUiType: "NoSurface",
hostPlatform: "IFrame",
isDisplayed: true,
isEmailCollectionEnabled: false, // to enable email collection
isFileUploadEnabled: false, // to enable file upload function
isScreenshotEnabled: false, // to enable screenshot
isScreenRecordingEnabled: false, // to enable screen recording
invokeOnDismissOnEsc: false,
isFeedbackForumEnabled: false,
isMyFeedbackEnabled: false,
isThankYouPageDisabled: false,
}

export const ratingFeedbackConfig: any = {
...baseConfig,
initialFeedbackType: "Unclassified",
scenarioConfig: {
isScenarioEnabled: true,
scenarioType: "Custom",
questionDetails: {
questionUiType: "Rating",
questionInstruction: {
srietkerk marked this conversation as resolved.
Show resolved Hide resolved
displayedStringInEnglish: "What did you think of this activity?",
displayedString: "What did you think of this activity?"
},
questionOptions: [
{
displayedStringInEnglish: "Boring",
displayedString: "Boring"
},
{
displayedStringInEnglish: "Not fun",
displayedString: "Not fun"
},
{
displayedStringInEnglish: "Kinda fun",
srietkerk marked this conversation as resolved.
Show resolved Hide resolved
displayedString: "Kinda fun"
},
{
displayedStringInEnglish: "Fun",
displayedString: "Fun"
},
{
displayedStringInEnglish: "Super fun",
displayedString: "Super fun"
},
],
"questionUiBehaviour": [
"CommentNotRequired"
]
}
}
}
3 changes: 3 additions & 0 deletions react-common/styles/controls/Feedback.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.feedback-modal iframe{
srietkerk marked this conversation as resolved.
Show resolved Hide resolved
border: none;
}
1 change: 1 addition & 0 deletions react-common/styles/react-common.less
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
@import "controls/VerticalSlider.less";
@import "controls/Accordion.less";
@import "controls/CarouselNav.less";
@import "controls/Feedback.less";
@import "./react-common-variables.less";

@import "fontawesome-free/less/solid.less";
Expand Down
Loading
Loading