mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2025-02-17 08:31:23 -05:00
Merge pull request #4714 from LLK/develop
Merge develop into release branch
This commit is contained in:
commit
191c881406
11 changed files with 585 additions and 387 deletions
712
package-lock.json
generated
712
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -130,7 +130,7 @@
|
|||
"redux-mock-store": "^1.2.3",
|
||||
"redux-thunk": "2.0.1",
|
||||
"sass-loader": "6.0.6",
|
||||
"scratch-gui": "0.1.0-prerelease.20201118213531",
|
||||
"scratch-gui": "0.1.0-prerelease.20201202090933",
|
||||
"scratch-l10n": "latest",
|
||||
"selenium-webdriver": "3.6.0",
|
||||
"slick-carousel": "1.6.0",
|
||||
|
|
|
@ -3,6 +3,8 @@ const PropTypes = require('prop-types');
|
|||
const React = require('react');
|
||||
|
||||
const Video = require('../video/video.jsx');
|
||||
const Spinner = require('../spinner/spinner.jsx');
|
||||
const classNames = require('classnames');
|
||||
|
||||
require('./video-preview.scss');
|
||||
|
||||
|
@ -10,16 +12,25 @@ class VideoPreview extends React.Component {
|
|||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleShowVideo'
|
||||
'handleShowVideo',
|
||||
'handleVideoLoaded'
|
||||
]);
|
||||
|
||||
this.state = {
|
||||
videoOpen: false
|
||||
videoOpen: false,
|
||||
spinnerVisible: false
|
||||
};
|
||||
}
|
||||
|
||||
handleShowVideo () {
|
||||
this.setState({videoOpen: true});
|
||||
this.setState({
|
||||
videoOpen: true,
|
||||
spinnerVisible: true
|
||||
});
|
||||
}
|
||||
|
||||
handleVideoLoaded () {
|
||||
this.setState({spinnerVisible: false});
|
||||
}
|
||||
|
||||
render () {
|
||||
|
@ -27,17 +38,26 @@ class VideoPreview extends React.Component {
|
|||
<div className="video-preview">
|
||||
{this.state.videoOpen ?
|
||||
(
|
||||
<Video
|
||||
className="video"
|
||||
height={this.props.videoHeight}
|
||||
videoId={this.props.videoId}
|
||||
width={this.props.videoWidth}
|
||||
/>
|
||||
<div className="spinner-video-container">
|
||||
{this.state.spinnerVisible ? <Spinner className="loading-spinner" /> : null}
|
||||
<Video
|
||||
className="video"
|
||||
height={this.props.videoHeight}
|
||||
videoId={this.props.videoId}
|
||||
width={this.props.videoWidth}
|
||||
onVideoStart={this.handleVideoLoaded}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="video-thumbnail"
|
||||
onClick={this.handleShowVideo}
|
||||
>
|
||||
{/* Load an invisible spinner so that the image has a chance to load before it's needed */}
|
||||
<img
|
||||
className={classNames('loading-spinner', 'hidden-spinner')}
|
||||
src="/svgs/modal/spinner-white.svg"
|
||||
/>
|
||||
<img
|
||||
src={this.props.thumbnail}
|
||||
style={{
|
||||
|
|
|
@ -24,3 +24,32 @@
|
|||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
margin: auto;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.hidden-spinner {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.spinner-video-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.iframe-video-not-started {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.iframe-video-started {
|
||||
visibility: visible;
|
||||
}
|
||||
|
|
|
@ -4,25 +4,65 @@ const classNames = require('classnames');
|
|||
|
||||
require('./video.scss');
|
||||
|
||||
const Video = props => (
|
||||
<div className={classNames('video-player', props.className)}>
|
||||
<iframe
|
||||
allowFullScreen
|
||||
// allowFullScreen is legacy, can we start using allow='fullscreen'?
|
||||
// allow="fullscreen"
|
||||
frameBorder="0" // deprecated attribute
|
||||
height={props.height}
|
||||
scrolling="no" // deprecated attribute
|
||||
src={`https://fast.wistia.net/embed/iframe/${props.videoId}?seo=false&videoFoam=true`}
|
||||
title={props.title}
|
||||
width={props.width}
|
||||
/>
|
||||
<script
|
||||
async
|
||||
src="https://fast.wistia.net/assets/external/E-v1.js"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
class Video extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
videoStarted: false
|
||||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
/**
|
||||
uses code snippets from
|
||||
https://github.com/mrdavidjcole/wistia-player-react/blob/master/src/components/wistia_embed.js
|
||||
**/
|
||||
if (!document.getElementById('wistia_script')) {
|
||||
const wistiaScript = document.createElement('script');
|
||||
wistiaScript.id = 'wistia_script';
|
||||
wistiaScript.type = 'text/javascript';
|
||||
wistiaScript.src = 'https://fast.wistia.com/assets/external/E-v1.js';
|
||||
wistiaScript.async = false;
|
||||
document.body.appendChild(wistiaScript);
|
||||
}
|
||||
|
||||
window._wq = window._wq || [];
|
||||
|
||||
// Use onReady in combination with the Wistia 'play' event handler so that onVideoStart()
|
||||
// isn't called until the video actually starts. onReady fires before the video player is visible.
|
||||
window._wq.push({
|
||||
id: this.props.videoId,
|
||||
onReady: video => {
|
||||
video.bind('play', () => {
|
||||
this.setState({videoStarted: true});
|
||||
this.props.onVideoStart();
|
||||
return video.unbind;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
// Provide CSS classes for anything using the video component to configure what happens before and after
|
||||
// the video has played for the first time. See VideoPreview for an example.
|
||||
const videoStartedClass = this.state.videoStarted ? 'iframe-video-started' : 'iframe-video-not-started';
|
||||
|
||||
return (
|
||||
<div className={classNames('video-player', this.props.className)}>
|
||||
<iframe
|
||||
allowFullScreen
|
||||
className={classNames('wistia_embed', `wistia_async_${this.props.videoId}`, videoStartedClass)}
|
||||
frameBorder="0" // deprecated attribute
|
||||
height={this.props.height}
|
||||
scrolling="no" // deprecated attribute
|
||||
src={`https://fast.wistia.net/embed/iframe/${this.props.videoId}?seo=false&videoFoam=true`}
|
||||
title={this.props.title}
|
||||
width={this.props.width}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Video.defaultProps = {
|
||||
height: '225',
|
||||
title: '',
|
||||
|
@ -32,6 +72,7 @@ Video.defaultProps = {
|
|||
Video.propTypes = {
|
||||
className: PropTypes.string,
|
||||
height: PropTypes.string.isRequired,
|
||||
onVideoStart: PropTypes.func,
|
||||
title: PropTypes.string.isRequired,
|
||||
videoId: PropTypes.string.isRequired,
|
||||
width: PropTypes.string.isRequired
|
||||
|
|
|
@ -275,6 +275,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.compose-disabled {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.comments-root-reply {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
|
|
@ -38,8 +38,8 @@ class ComposeComment extends React.Component {
|
|||
'handleCancel',
|
||||
'handleInput',
|
||||
'handleMuteClose',
|
||||
'handleMuteOpen'
|
||||
|
||||
'handleMuteOpen',
|
||||
'isMuted'
|
||||
]);
|
||||
this.state = {
|
||||
message: '',
|
||||
|
@ -115,6 +115,10 @@ class ComposeComment extends React.Component {
|
|||
return Math.ceil(((timeStampInSec * 1000) - Date.now()) / (60 * 1000));
|
||||
}
|
||||
|
||||
isMuted () {
|
||||
return this.state.muteExpiresAt * 1000 > Date.now();
|
||||
}
|
||||
|
||||
handleMuteClose () {
|
||||
this.setState({
|
||||
muteOpen: false
|
||||
|
@ -163,10 +167,10 @@ class ComposeComment extends React.Component {
|
|||
render () {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.state.status === ComposeStatus.REJECTED_MUTE ? (
|
||||
{this.isMuted() ? (
|
||||
<FlexRow className="comment">
|
||||
<CommentingStatus>
|
||||
<p>Scratch thinks your comment was disrespectful.</p>
|
||||
<p>Scratch thinks your most recent comment was disrespectful.</p>
|
||||
<p>
|
||||
For the next {this.convertToMinutesFromNow(this.state.muteExpiresAt)} minutes you
|
||||
won't be able to post comments.
|
||||
|
@ -182,7 +186,10 @@ class ComposeComment extends React.Component {
|
|||
</FlexRow>
|
||||
) : null }
|
||||
<div
|
||||
className="flex-row comment"
|
||||
className={classNames('flex-row',
|
||||
'comment',
|
||||
this.state.status === ComposeStatus.REJECTED_MUTE ?
|
||||
'compose-disabled' : '')}
|
||||
>
|
||||
<a href={`/users/${this.props.user.username}`}>
|
||||
<Avatar src={this.props.user.thumbnailUrl} />
|
||||
|
@ -205,6 +212,7 @@ class ComposeComment extends React.Component {
|
|||
className={classNames('compose-input',
|
||||
MAX_COMMENT_LENGTH - this.state.message.length >= 0 ?
|
||||
'compose-valid' : 'compose-invalid')}
|
||||
disabled={this.state.status === ComposeStatus.REJECTED_MUTE}
|
||||
handleUpdate={onUpdate}
|
||||
name="compose-comment"
|
||||
type="textarea"
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"sec.subscribeCallToAction": "sign up for our mailing list",
|
||||
"sec.applyDeadline": "The deadline for applying to the Scratch Education Collaborative is January 10th, 2021",
|
||||
"sec.applyButton": "Click here to apply",
|
||||
"sec.projectsIntro": "The Scratch Foundation is launching the Scratch Education Collaborative (SEC), to bring together organizations committed to supporting creative coding experiences with a focus on educators, students, and communities historically underrepresented in computing.",
|
||||
"sec.projectsIntro": "The Scratch Foundation is launching the Scratch Education Collaborative (SEC), to bring together organizations committed to supporting creative coding experiences with a focus on educators, students, and communities historically excluded from computing.",
|
||||
"sec.projectsIntro2": "In 2021, during the pilot year of the SEC, 5 organizations from across the globe will be selected to share their work, learn from one another, and help to develop best practices and examples for implementing {culturallySustainingLink} creative computing with Scratch.",
|
||||
"sec.culturallySustaining": "culturally sustaining",
|
||||
"sec.expectationsFromSec": "What participating organizations can expect from the SEC",
|
||||
|
@ -22,7 +22,7 @@
|
|||
"sec.expectationsFromOrgsPoint3": "Host at least one event, tutorial, or professional development activity for your community in 2021",
|
||||
"sec.expectationsFromOrgsPoint4": "Share best-practices, learnings, and challenges back with the Scratch Foundation and SEC peer organizations",
|
||||
"sec.eligibilityPoint1": "Participants must be a non-profit organization, public school, school district, university, college, or government entity",
|
||||
"sec.eligibilityPoint2": "Organizations must be part-of and work with communities historically underrepresented in computing",
|
||||
"sec.eligibilityPoint2": "Organizations must be part-of and work with communities historically excluded from computing",
|
||||
"sec.eligibilityPoint3": "Must be able to dedicate at least one staff person as point of contact for the program",
|
||||
"sec.eligibilityPrompt": "Wondering if your organization might be a good fit for the pilot year of the Scratch Education Collaborative? {link} to find out more.",
|
||||
"sec.eligibilityPromptLink": "Read the FAQ"
|
||||
|
|
|
@ -341,7 +341,7 @@ const Landing = props => (
|
|||
id="teacherlanding.accountsRequestInfo"
|
||||
values={{
|
||||
setupGuideLink: (
|
||||
<a href="https://docs.google.com/document/d/1Qb8Lyeiivr-oB49p5Bo17iXU5qxGpBJHuFa_KR5aW-o/view" >
|
||||
<a href="https://resources.scratch.mit.edu/www/guides/en/scratch-teacher-accounts-guide.pdf" >
|
||||
<FormattedMessage id="teacherlanding.accountsSetupGuide" />
|
||||
</a>
|
||||
),
|
||||
|
|
Binary file not shown.
|
@ -66,23 +66,73 @@ describe('Compose Comment test', () => {
|
|||
expect(component.find('FlexRow.compose-error-row').exists()).toEqual(false);
|
||||
});
|
||||
|
||||
test('Comment Status shows when state is REJECTED_MUTE ', () => {
|
||||
test('Comment Status shows when mute expiration in the future ', () => {
|
||||
const realDateNow = Date.now.bind(global.Date);
|
||||
global.Date.now = () => 0;
|
||||
const component = getComposeCommentWrapper({});
|
||||
const commentInstance = component.instance();
|
||||
commentInstance.setState({status: 'REJECTED_MUTE'});
|
||||
commentInstance.setState({muteExpiresAt: 100});
|
||||
component.update();
|
||||
expect(component.find('FlexRow.compose-comment').exists()).toEqual(true);
|
||||
expect(component.find('MuteModal').exists()).toEqual(false);
|
||||
expect(component.find('CommentingStatus').exists()).toEqual(true);
|
||||
global.Date.now = realDateNow;
|
||||
});
|
||||
test('Comment Status shows when user just submitted a comment that got them muted', () => {
|
||||
const realDateNow = Date.now.bind(global.Date);
|
||||
global.Date.now = () => 0;
|
||||
const component = getComposeCommentWrapper({});
|
||||
const commentInstance = component.instance();
|
||||
commentInstance.setState({
|
||||
status: 'REJECTED_MUTE',
|
||||
muteExpiresAt: 100
|
||||
});
|
||||
component.update();
|
||||
expect(component.find('FlexRow.compose-comment').exists()).toEqual(true);
|
||||
expect(component.find('MuteModal').exists()).toEqual(false);
|
||||
expect(component.find('CommentingStatus').exists()).toEqual(true);
|
||||
// Compose box is disabled
|
||||
expect(component.find('InplaceInput.compose-input').exists()).toEqual(true);
|
||||
expect(component.find('InplaceInput.compose-input').props().disabled).toBe(true);
|
||||
global.Date.now = realDateNow;
|
||||
});
|
||||
test('Comment Error does not show for mutes', () => {
|
||||
const realDateNow = Date.now.bind(global.Date);
|
||||
global.Date.now = () => 0;
|
||||
const component = getComposeCommentWrapper({});
|
||||
const commentInstance = component.instance();
|
||||
commentInstance.setState({
|
||||
status: 'REJECTED_MUTE',
|
||||
error: 'a mute error'
|
||||
});
|
||||
component.update();
|
||||
expect(component.find('FlexRow.compose-error-row').exists()).toEqual(false);
|
||||
expect(component.find('FlexRow.compose-comment').exists()).toEqual(true);
|
||||
global.Date.now = realDateNow;
|
||||
});
|
||||
test('Comment Error does show for non-mute errors', () => {
|
||||
const component = getComposeCommentWrapper({});
|
||||
const commentInstance = component.instance();
|
||||
commentInstance.setState({
|
||||
error: 'some error',
|
||||
status: 'FLOOD'
|
||||
});
|
||||
component.update();
|
||||
expect(component.find('FlexRow.compose-error-row').exists()).toEqual(true);
|
||||
expect(component.find('FlexRow.compose-comment').exists()).toEqual(true);
|
||||
expect(component.find('InplaceInput.compose-input').exists()).toEqual(true);
|
||||
expect(component.find('InplaceInput.compose-input').props().disabled).toBe(false);
|
||||
});
|
||||
|
||||
test('Mute Modal shows when muteOpen is true ', () => {
|
||||
const realDateNow = Date.now.bind(global.Date);
|
||||
global.Date.now = () => 0;
|
||||
const component = getComposeCommentWrapper({});
|
||||
const commentInstance = component.instance();
|
||||
commentInstance.setState({muteOpen: true});
|
||||
component.update();
|
||||
expect(component.find('FlexRow.compose-comment').exists()).toEqual(true);
|
||||
expect(component.find('MuteModal').exists()).toEqual(true);
|
||||
global.Date.now = realDateNow;
|
||||
});
|
||||
|
||||
test('shouldShowMuteModal is false when list is undefined ', () => {
|
||||
|
@ -130,4 +180,32 @@ describe('Compose Comment test', () => {
|
|||
global.Date.now = realDateNow;
|
||||
});
|
||||
|
||||
test('isMuted: expiration is in the future ', () => {
|
||||
const realDateNow = Date.now.bind(global.Date);
|
||||
global.Date.now = () => 0; // Set "now" to 0 for easier testing.
|
||||
|
||||
const commentInstance = getComposeCommentWrapper({}).instance();
|
||||
commentInstance.setState({muteExpiresAt: 100});
|
||||
expect(commentInstance.isMuted()).toBe(true);
|
||||
global.Date.now = realDateNow;
|
||||
});
|
||||
|
||||
test('isMuted: expiration is in the past ', () => {
|
||||
const realDateNow = Date.now.bind(global.Date);
|
||||
global.Date.now = () => 0;
|
||||
|
||||
const commentInstance = getComposeCommentWrapper({}).instance();
|
||||
commentInstance.setState({muteExpiresAt: -100});
|
||||
expect(commentInstance.isMuted()).toBe(false);
|
||||
global.Date.now = realDateNow;
|
||||
});
|
||||
|
||||
test('isMuted: expiration is not set ', () => {
|
||||
const realDateNow = Date.now.bind(global.Date);
|
||||
global.Date.now = () => 0;
|
||||
|
||||
const commentInstance = getComposeCommentWrapper({}).instance();
|
||||
expect(commentInstance.isMuted()).toBe(false);
|
||||
global.Date.now = realDateNow;
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue