Merge pull request #8676 from scratchfoundation/release/2024-09-04
Some checks are pending
CI/CD / build-and-test-and-maybe-deploy (push) Waiting to run

[Master] release/2024-09-04
This commit is contained in:
Christopher Willis-Ford 2024-09-05 07:29:47 -07:00 committed by GitHub
commit e19c54e4ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1296 additions and 1551 deletions

View file

@ -68,6 +68,8 @@ jobs:
PROJECT_HOST: ${{ secrets.PROJECT_HOST }} PROJECT_HOST: ${{ secrets.PROJECT_HOST }}
STATIC_HOST: ${{ secrets.STATIC_HOST }} STATIC_HOST: ${{ secrets.STATIC_HOST }}
SCRATCH_ENV: ${{ vars.SCRATCH_ENV }} SCRATCH_ENV: ${{ vars.SCRATCH_ENV }}
SORTING_HAT_HOST: ${{ vars.SORTING_HAT_HOST }}
USER_GUIDING_ID: ${{ secrets.USER_GUIDING_ID }}
# used by src/template-config.js # used by src/template-config.js
GTM_ID: ${{ secrets.GTM_ID }} GTM_ID: ${{ secrets.GTM_ID }}

2
.nvmrc
View file

@ -1 +1 @@
18.20.3 18.20.4

2577
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -55,27 +55,27 @@
"react-onclickoutside": "6.13.0", "react-onclickoutside": "6.13.0",
"react-router-dom": "5.3.4", "react-router-dom": "5.3.4",
"react-twitter-embed": "3.0.3", "react-twitter-embed": "3.0.3",
"react-use": "17.5.0", "react-use": "17.5.1",
"scratch-parser": "5.2.1", "scratch-parser": "5.2.1",
"scratch-storage": "2.3.136" "scratch-storage": "2.3.234"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "7.24.6", "@babel/cli": "7.25.6",
"@babel/core": "7.24.6", "@babel/core": "7.25.2",
"@babel/eslint-parser": "7.24.6", "@babel/eslint-parser": "7.25.1",
"@babel/plugin-syntax-dynamic-import": "7.8.3", "@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/plugin-transform-async-to-generator": "7.24.6", "@babel/plugin-transform-async-to-generator": "7.24.7",
"@babel/plugin-transform-object-rest-spread": "7.24.6", "@babel/plugin-transform-object-rest-spread": "7.24.7",
"@babel/preset-env": "7.24.6", "@babel/preset-env": "7.25.4",
"@babel/preset-react": "7.24.6", "@babel/preset-react": "7.24.7",
"@formatjs/intl-datetimeformat": "6.12.5", "@formatjs/intl-datetimeformat": "6.12.5",
"@formatjs/intl-locale": "3.4.6", "@formatjs/intl-locale": "3.4.6",
"@formatjs/intl-numberformat": "8.10.3", "@formatjs/intl-numberformat": "8.10.3",
"@formatjs/intl-pluralrules": "5.2.14", "@formatjs/intl-pluralrules": "5.2.14",
"@formatjs/intl-relativetimeformat": "11.2.14", "@formatjs/intl-relativetimeformat": "11.2.14",
"@types/jest": "29.5.12", "@types/jest": "29.5.12",
"async": "3.2.5", "async": "3.2.6",
"autoprefixer": "10.4.19", "autoprefixer": "10.4.20",
"babel-loader": "8.3.0", "babel-loader": "8.3.0",
"babel-plugin-transform-require-context": "0.1.1", "babel-plugin-transform-require-context": "0.1.1",
"bowser": "1.9.4", "bowser": "1.9.4",
@ -93,15 +93,16 @@
"eslint-config-scratch": "9.0.8", "eslint-config-scratch": "9.0.8",
"eslint-plugin-jest": "27.9.0", "eslint-plugin-jest": "27.9.0",
"eslint-plugin-json": "2.1.2", "eslint-plugin-json": "2.1.2",
"eslint-plugin-react": "7.34.2", "eslint-plugin-react": "7.35.0",
"eslint-plugin-react-hooks": "4.6.2", "eslint-plugin-react-hooks": "4.6.2",
"fastly": "1.2.1", "fastly": "1.2.1",
"file-loader": "6.2.0",
"formik": "1.5.8", "formik": "1.5.8",
"formsy-react": "1.1.6", "formsy-react": "1.1.6",
"formsy-react-components": "1.1.0", "formsy-react-components": "1.1.0",
"git-bundle-sha": "0.0.2", "git-bundle-sha": "0.0.2",
"glob": "5.0.15", "glob": "5.0.15",
"google-libphonenumber": "3.2.34", "google-libphonenumber": "3.2.38",
"html-webpack-plugin": "5.6.0", "html-webpack-plugin": "5.6.0",
"iso-3166-2": "1.0.0", "iso-3166-2": "1.0.0",
"jest": "29.7.0", "jest": "29.7.0",
@ -115,12 +116,15 @@
"lodash.mergewith": "4.6.2", "lodash.mergewith": "4.6.2",
"lodash.omit": "3.1.0", "lodash.omit": "3.1.0",
"lodash.uniqby": "4.7.0", "lodash.uniqby": "4.7.0",
"lodash.sample": "4.2.1",
"mini-css-extract-plugin": "1.6.2", "mini-css-extract-plugin": "1.6.2",
"minilog": "2.1.0", "minilog": "2.1.0",
"pako": "0.2.8", "pako": "0.2.8",
"plotly.js": "1.47.4", "plotly.js": "1.47.4",
"postcss": "8.4.38", "postcss-import": "12.0.1",
"postcss": "8.4.40",
"postcss-loader": "4.3.0", "postcss-loader": "4.3.0",
"postcss-simple-vars": "5.0.2",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"query-string": "5.1.1", "query-string": "5.1.1",
"react": "16.14.0", "react": "16.14.0",
@ -138,18 +142,18 @@
"redux-mock-store": "1.5.4", "redux-mock-store": "1.5.4",
"redux-thunk": "2.4.2", "redux-thunk": "2.4.2",
"regenerator-runtime": "0.13.9", "regenerator-runtime": "0.13.9",
"sass": "1.77.4", "sass": "1.77.8",
"sass-loader": "10.5.2", "sass-loader": "10.5.2",
"scratch-gui": "3.6.15", "scratch-gui": "4.0.17",
"scratch-l10n": "3.18.168", "scratch-l10n": "3.18.290",
"selenium-webdriver": "4.21.0", "selenium-webdriver": "4.24.0",
"slick-carousel": "1.8.1", "slick-carousel": "1.8.1",
"stream-browserify": "3.0.0", "stream-browserify": "3.0.0",
"style-loader": "0.12.3", "style-loader": "4.0.0",
"tap": "14.11.0", "tap": "14.11.0",
"url-loader": "3.0.0", "url-loader": "3.0.0",
"use-onclickoutside": "0.4.1", "use-onclickoutside": "0.4.1",
"webpack": "5.91.0", "webpack": "5.94.0",
"webpack-bundle-analyzer": "4.10.2", "webpack-bundle-analyzer": "4.10.2",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"webpack-dev-middleware": "5.3.4", "webpack-dev-middleware": "5.3.4",

124
src/lib/user-guiding.js Normal file
View file

@ -0,0 +1,124 @@
const api = require('./api');
const sample = require('lodash.sample');
const USER_GUIDING_ID = process.env.USER_GUIDING_ID;
const AUTONOMY_SURVEY_ID = 3677;
const RELATIONSHIP_SURVEY_ID = 3678;
const JOY_SURVEY_ID = 3679;
const COMPETENCE_SURVEY_ID = 3676;
const EDITOR_INTERACTION_SURVEY_IDS = [COMPETENCE_SURVEY_ID, JOY_SURVEY_ID];
const CONDITIONS = {condition_list: [
'IsLoggedIn',
'NotAdmin',
'NotMuted'
]};
const USER_GUIDING_SNIPPET = `
(function(g, u, i, d, e, s) {
g[e] = g[e] || [];
var f = u.getElementsByTagName(i)[0];
var k = u.createElement(i);
k.async = true;
k.src = 'https://static.userguiding.com/media/user-guiding-' + s + '-embedded.js';
f.parentNode.insertBefore(k, f);
if (g[d]) return;
var ug = g[d] = {
q: []
};
ug.c = function(n) {
return function() {
ug.q.push([n, arguments])
};
};
var m = ['previewGuide', 'finishPreview', 'track', 'identify', 'hideChecklist', 'launchChecklist'];
for (var j = 0; j < m.length; j += 1) {
ug[m[j]] = ug.c(m[j]);
}
})(window, document, 'script', 'userGuiding', 'userGuidingLayer', '${USER_GUIDING_ID}');
`;
const activateUserGuiding = (userId, callback) => {
if (window.userGuiding) {
callback();
return;
}
const userGuidingScript = document.createElement('script');
userGuidingScript.innerHTML = USER_GUIDING_SNIPPET;
document.head.insertBefore(userGuidingScript, document.head.firstChild);
window.userGuidingSettings = {disablePageViewAutoCapture: true};
window.userGuidingLayer.push({
event: 'onload',
fn: () => window.userGuiding.identify(userId.toString())
});
window.userGuidingLayer.push({
event: 'onIdentificationComplete',
fn: callback
});
};
const attemptDisplayUserGuidingSurvey = (userId, permissions, guideId, callback) => {
if (!USER_GUIDING_ID || !process.env.SORTING_HAT_HOST) {
return;
}
api({
uri: '/user_guiding',
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-USERID': userId,
'X-PERMISSIONS': JSON.stringify(permissions),
'X-CONDITIONS': JSON.stringify(CONDITIONS),
'X-QUESTION-NUMBER': guideId
},
host: process.env.SORTING_HAT_HOST
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
return;
}
if (body?.result === 'true') {
activateUserGuiding(userId, callback);
}
});
};
const onCommented = (userId, permissions) => {
attemptDisplayUserGuidingSurvey(
userId,
permissions,
AUTONOMY_SURVEY_ID,
() => window.userGuiding.launchSurvey(AUTONOMY_SURVEY_ID)
);
};
const onProjectShared = (userId, permissions) => {
attemptDisplayUserGuidingSurvey(
userId,
permissions,
RELATIONSHIP_SURVEY_ID,
() => window.userGuiding.launchSurvey(RELATIONSHIP_SURVEY_ID)
);
};
const onProjectLoaded = (userId, permissions) => {
const surveyId = sample(EDITOR_INTERACTION_SURVEY_IDS);
attemptDisplayUserGuidingSurvey(
userId,
permissions,
surveyId,
() => window.userGuiding.launchSurvey(surveyId)
);
};
module.exports = {
onProjectLoaded,
onCommented,
onProjectShared
};

View file

@ -34,11 +34,13 @@ const thumbnailUrl = require('../../lib/user-thumbnail');
const FormsyProjectUpdater = require('./formsy-project-updater.jsx'); const FormsyProjectUpdater = require('./formsy-project-updater.jsx');
const EmailConfirmationModal = require('../../components/modal/email-confirmation/modal.jsx'); const EmailConfirmationModal = require('../../components/modal/email-confirmation/modal.jsx');
const EmailConfirmationBanner = require('../../components/dropdown-banner/email-confirmation/banner.jsx'); const EmailConfirmationBanner = require('../../components/dropdown-banner/email-confirmation/banner.jsx');
const {onCommented} = require('../../lib/user-guiding.js');
const projectShape = require('./projectshape.jsx').projectShape; const projectShape = require('./projectshape.jsx').projectShape;
require('./preview.scss'); require('./preview.scss');
const frameless = require('../../lib/frameless'); const frameless = require('../../lib/frameless');
const {useState, useCallback} = require('react');
// disable enter key submission on formsy input fields; otherwise formsy thinks // disable enter key submission on formsy input fields; otherwise formsy thinks
// we meant to trigger the "See inside" button. Instead, treat these keypresses // we meant to trigger the "See inside" button. Instead, treat these keypresses
@ -127,6 +129,7 @@ const PreviewPresentation = ({
showCloudDataAlert, showCloudDataAlert,
showCloudDataAndVideoAlert, showCloudDataAndVideoAlert,
showUsernameBlockAlert, showUsernameBlockAlert,
permissions,
projectHost, projectHost,
projectId, projectId,
projectInfo, projectInfo,
@ -140,9 +143,11 @@ const PreviewPresentation = ({
showEmailConfirmationBanner, showEmailConfirmationBanner,
singleCommentId, singleCommentId,
socialOpen, socialOpen,
user,
userOwnsProject, userOwnsProject,
visibilityInfo visibilityInfo
}) => { }) => {
const [hasSubmittedComment, setHasSubmittedComment] = useState(false);
const shareDate = ((projectInfo.history && projectInfo.history.shared)) ? projectInfo.history.shared : ''; const shareDate = ((projectInfo.history && projectInfo.history.shared)) ? projectInfo.history.shared : '';
const revisedDate = ((projectInfo.history && projectInfo.history.modified)) ? projectInfo.history.modified : ''; const revisedDate = ((projectInfo.history && projectInfo.history.modified)) ? projectInfo.history.modified : '';
const showInstructions = editable || projectInfo.instructions || const showInstructions = editable || projectInfo.instructions ||
@ -215,6 +220,15 @@ const PreviewPresentation = ({
))} ))}
</FlexRow> </FlexRow>
); );
const onAddCommentWrapper = useCallback(body => {
onAddComment(body);
if (!hasSubmittedComment && user) {
setHasSubmittedComment(true);
onCommented(user.id, permissions);
}
}, [hasSubmittedComment, user]);
return ( return (
<div className="preview"> <div className="preview">
{showEmailConfirmationModal && <EmailConfirmationModal {showEmailConfirmationModal && <EmailConfirmationModal
@ -610,7 +624,7 @@ const PreviewPresentation = ({
isLoggedIn ? ( isLoggedIn ? (
isShared && <ComposeComment isShared && <ComposeComment
postURI={`/proxy/comments/project/${projectId}`} postURI={`/proxy/comments/project/${projectId}`}
onAddComment={onAddComment} onAddComment={onAddCommentWrapper}
/> />
) : ( ) : (
/* TODO add box for signing in to leave a comment */ /* TODO add box for signing in to leave a comment */
@ -784,6 +798,7 @@ PreviewPresentation.propTypes = {
onUpdateProjectThumbnail: PropTypes.func, onUpdateProjectThumbnail: PropTypes.func,
originalInfo: projectShape, originalInfo: projectShape,
parentInfo: projectShape, parentInfo: projectShape,
permissions: PropTypes.object,
projectHost: PropTypes.string, projectHost: PropTypes.string,
projectId: PropTypes.string, projectId: PropTypes.string,
projectInfo: projectShape, projectInfo: projectShape,
@ -800,6 +815,9 @@ PreviewPresentation.propTypes = {
showUsernameBlockAlert: PropTypes.bool, showUsernameBlockAlert: PropTypes.bool,
singleCommentId: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]), singleCommentId: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
socialOpen: PropTypes.bool, socialOpen: PropTypes.bool,
user: PropTypes.shape({
id: PropTypes.number
}),
userOwnsProject: PropTypes.bool, userOwnsProject: PropTypes.bool,
visibilityInfo: PropTypes.shape({ visibilityInfo: PropTypes.shape({
censored: PropTypes.bool, censored: PropTypes.bool,

View file

@ -25,6 +25,10 @@ const ConnectedLogin = require('../../components/login/connected-login.jsx');
const CanceledDeletionModal = require('../../components/login/canceled-deletion-modal.jsx'); const CanceledDeletionModal = require('../../components/login/canceled-deletion-modal.jsx');
const NotAvailable = require('../../components/not-available/not-available.jsx'); const NotAvailable = require('../../components/not-available/not-available.jsx');
const Meta = require('./meta.jsx'); const Meta = require('./meta.jsx');
const {
onProjectShared,
onProjectLoaded
} = require('../../lib/user-guiding.js');
const sessionActions = require('../../redux/session.js'); const sessionActions = require('../../redux/session.js');
const {selectProjectCommentsGloballyEnabled, selectIsTotallyNormal} = require('../../redux/session'); const {selectProjectCommentsGloballyEnabled, selectIsTotallyNormal} = require('../../redux/session');
@ -40,6 +44,25 @@ const IntlGUI = injectIntl(GUI.default);
const localStorageAvailable = 'localStorage' in window && window.localStorage !== null; const localStorageAvailable = 'localStorage' in window && window.localStorage !== null;
const xhr = require('xhr'); const xhr = require('xhr');
const {useEffect} = require('react');
const IntlGUIWithProjectHandler = ({user, permissions, ...props}) => {
useEffect(() => {
if (props.projectId && props.projectId !== '0') {
onProjectLoaded(user.id, permissions);
}
}, [props.projectId, user.id, permissions]);
return <IntlGUI {...props} />;
};
IntlGUIWithProjectHandler.propTypes = {
...GUI.propTypes,
user: PropTypes.shape({
id: PropTypes.number
}),
permissions: PropTypes.object
};
class Preview extends React.Component { class Preview extends React.Component {
constructor (props) { constructor (props) {
@ -627,6 +650,7 @@ class Preview extends React.Component {
justRemixed: false, justRemixed: false,
justShared: true justShared: true
}); });
onProjectShared(this.props.user.id, this.props.permissions);
} }
handleShareAttempt () { handleShareAttempt () {
this.setState({ this.setState({
@ -786,6 +810,7 @@ class Preview extends React.Component {
moreCommentsToLoad={this.props.moreCommentsToLoad} moreCommentsToLoad={this.props.moreCommentsToLoad}
originalInfo={this.props.original} originalInfo={this.props.original}
parentInfo={this.props.parent} parentInfo={this.props.parent}
permissions={this.props.permissions}
projectHost={this.props.projectHost} projectHost={this.props.projectHost}
projectId={this.state.projectId} projectId={this.state.projectId}
projectInfo={this.props.projectInfo} projectInfo={this.props.projectInfo}
@ -802,6 +827,7 @@ class Preview extends React.Component {
showUsernameBlockAlert={this.state.showUsernameBlockAlert} showUsernameBlockAlert={this.state.showUsernameBlockAlert}
singleCommentId={this.state.singleCommentId} singleCommentId={this.state.singleCommentId}
socialOpen={this.state.socialOpen} socialOpen={this.state.socialOpen}
user={this.props.user}
userOwnsProject={this.props.userOwnsProject} userOwnsProject={this.props.userOwnsProject}
visibilityInfo={this.props.visibilityInfo} visibilityInfo={this.props.visibilityInfo}
onAddComment={this.handleAddComment} onAddComment={this.handleAddComment}
@ -841,7 +867,7 @@ class Preview extends React.Component {
</Page> : </Page> :
<React.Fragment> <React.Fragment>
{showGUI && ( {showGUI && (
<IntlGUI <IntlGUIWithProjectHandler
assetHost={this.props.assetHost} assetHost={this.props.assetHost}
authorId={this.props.authorId} authorId={this.props.authorId}
authorThumbnailUrl={this.props.authorThumbnailUrl} authorThumbnailUrl={this.props.authorThumbnailUrl}
@ -879,6 +905,8 @@ class Preview extends React.Component {
onUpdateProjectId={this.handleUpdateProjectId} onUpdateProjectId={this.handleUpdateProjectId}
onUpdateProjectThumbnail={this.props.handleUpdateProjectThumbnail} onUpdateProjectThumbnail={this.props.handleUpdateProjectThumbnail}
onUpdateProjectTitle={this.handleUpdateProjectTitle} onUpdateProjectTitle={this.handleUpdateProjectTitle}
user={this.props.user}
permissions={this.props.permissions}
/> />
)} )}
{this.props.registrationOpen && ( {this.props.registrationOpen && (
@ -957,6 +985,7 @@ Preview.propTypes = {
moreCommentsToLoad: PropTypes.bool, moreCommentsToLoad: PropTypes.bool,
original: projectShape, original: projectShape,
parent: projectShape, parent: projectShape,
permissions: PropTypes.object,
playerMode: PropTypes.bool, playerMode: PropTypes.bool,
projectHost: PropTypes.string.isRequired, projectHost: PropTypes.string.isRequired,
projectInfo: projectShape, projectInfo: projectShape,
@ -1076,6 +1105,7 @@ const mapStateToProps = state => {
moreCommentsToLoad: state.comments.moreCommentsToLoad, moreCommentsToLoad: state.comments.moreCommentsToLoad,
original: state.preview.original, original: state.preview.original,
parent: state.preview.parent, parent: state.preview.parent,
permissions: state.permissions,
playerMode: state.scratchGui.mode.isPlayerOnly, playerMode: state.scratchGui.mode.isPlayerOnly,
projectInfo: state.preview.projectInfo, projectInfo: state.preview.projectInfo,
projectNotAvailable: state.preview.projectNotAvailable, projectNotAvailable: state.preview.projectNotAvailable,

View file

@ -123,7 +123,20 @@ module.exports = {
/node_modules[\\/]scratch-[^\\/]+[\\/]src/, /node_modules[\\/]scratch-[^\\/]+[\\/]src/,
/node_modules[\\/]pify/, /node_modules[\\/]pify/,
/node_modules[\\/]async/ /node_modules[\\/]async/
] ],
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
},
{
test: /\.hex$/,
type: 'asset/resource',
use: [{
loader: 'url-loader',
options: {
limit: 16 * 1024
}
}]
}, },
{ {
test: /\.scss$/, test: /\.scss$/,
@ -151,20 +164,29 @@ module.exports = {
{ {
test: /\.css$/, test: /\.css$/,
use: [ use: [
MiniCssExtractPlugin.loader, {
loader: 'style-loader'
},
{ {
loader: 'css-loader', loader: 'css-loader',
options: { options: {
url: false modules: {
localIdentName: '[name]_[local]_[hash:base64:5]',
exportLocalsConvention: 'camelCase'
},
importLoaders: 1,
esModule: false
} }
}, },
{ {
loader: 'postcss-loader', loader: 'postcss-loader',
options: { options: {
postcssOptions: { postcssOptions: {
plugins: function () { plugins: [
return [autoprefixer()]; 'postcss-import',
} 'postcss-simple-vars',
'autoprefixer'
]
} }
} }
} }
@ -253,7 +275,13 @@ module.exports = {
'process.env.STATIC_HOST': `"${process.env.STATIC_HOST || 'https://uploads.scratch.mit.edu'}"`, 'process.env.STATIC_HOST': `"${process.env.STATIC_HOST || 'https://uploads.scratch.mit.edu'}"`,
'process.env.SCRATCH_ENV': `"${process.env.SCRATCH_ENV || 'development'}"`, 'process.env.SCRATCH_ENV': `"${process.env.SCRATCH_ENV || 'development'}"`,
'process.env.THUMBNAIL_URI': `"${process.env.THUMBNAIL_URI || '/internalapi/project/thumbnail/{}/set/'}"`, 'process.env.THUMBNAIL_URI': `"${process.env.THUMBNAIL_URI || '/internalapi/project/thumbnail/{}/set/'}"`,
'process.env.THUMBNAIL_HOST': `"${process.env.THUMBNAIL_HOST || ''}"` 'process.env.THUMBNAIL_HOST': `"${process.env.THUMBNAIL_HOST || ''}"`,
'process.env.DEBUG': Boolean(process.env.DEBUG),
'process.env.GA_ID': `"${process.env.GA_ID || 'UA-000000-01'}"`,
'process.env.GTM_ENV_AUTH': `"${process.env.GTM_ENV_AUTH || ''}"`,
'process.env.GTM_ID': process.env.GTM_ID ? `"${process.env.GTM_ID}"` : null,
'process.env.USER_GUIDING_ID': `"${process.env.USER_GUIDING_ID || ''}"`,
'process.env.SORTING_HAT_HOST': `"${process.env.SORTING_HAT_HOST || ''}"`
}) })
]) ])
.concat(process.env.ANALYZE_BUNDLE === 'true' ? [ .concat(process.env.ANALYZE_BUNDLE === 'true' ? [