From a3a22a087a4d4c74dccb256ed7585218c6474cea Mon Sep 17 00:00:00 2001 From: apple502j <33279053+apple502j@users.noreply.github.com> Date: Wed, 26 Feb 2020 13:52:17 +0900 Subject: [PATCH 01/20] Localize activity messages --- src/views/splash/l10n.json | 5 ++++- src/views/splash/presentation.jsx | 10 ++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/views/splash/l10n.json b/src/views/splash/l10n.json index 203bbe276..7ecb0fbc6 100644 --- a/src/views/splash/l10n.json +++ b/src/views/splash/l10n.json @@ -42,5 +42,8 @@ "welcome.welcomeToScratch": "Welcome to Scratch!", "welcome.learn": "Learn how to make a project in Scratch", "welcome.tryOut": "Try out starter projects", - "welcome.connect": "Connect with other Scratchers" + "welcome.connect": "Connect with other Scratchers", + + "activity.seeUpdates": "This is where you will see updates from Scratchers you follow", + "activity.checkOutScratchers": "Check out some Scratchers you might like to follow" } diff --git a/src/views/splash/presentation.jsx b/src/views/splash/presentation.jsx index 9a973e59b..030bb1b70 100644 --- a/src/views/splash/presentation.jsx +++ b/src/views/splash/presentation.jsx @@ -178,16 +178,10 @@ class ActivityList extends React.Component { key="activity-empty" >

- +

- + ]} From 75f8c6429abf3b6017aca92803d1f83d92799725 Mon Sep 17 00:00:00 2001 From: Ben Wheeler Date: Thu, 2 Apr 2020 00:12:56 -0400 Subject: [PATCH 02/20] added join token route; handle route alongside existing student signup uri --- src/routes.json | 7 ++++ .../studentregistration.jsx | 42 ++++++++++++++----- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/routes.json b/src/routes.json index ebc57e309..781348730 100644 --- a/src/routes.json +++ b/src/routes.json @@ -280,6 +280,13 @@ "view": "studentregistration/studentregistration", "title": "Class Registration" }, + { + "name": "student-registration-token-only", + "pattern": "^/join/:token", + "routeAlias": "/classes/(complete_registration|.+/register/.+)", + "view": "studentregistration/studentregistration", + "title": "Class Registration" + }, { "name": "teacher-faq", "pattern": "^/educators/faq/?$", diff --git a/src/views/studentregistration/studentregistration.jsx b/src/views/studentregistration/studentregistration.jsx index 5f93d3c73..78443686b 100644 --- a/src/views/studentregistration/studentregistration.jsx +++ b/src/views/studentregistration/studentregistration.jsx @@ -32,11 +32,22 @@ class StudentRegistration extends React.Component { } componentDidMount () { this.setState({waiting: true}); // eslint-disable-line react/no-did-mount-set-state + + // set uri and params + let uri; + let params; + if (this.props.classroomId === null || typeof this.props.classroomId === 'undefined') { + // configure for token-only endpoint + uri = `/classtoken/${this.props.classroomToken}`; + } else { + // configure for endpoint expecting classroomId and token + uri = `/classrooms/${this.props.classroomId}`; + params = {token: this.props.classroomToken}; + } + api({ - uri: `/classrooms/${this.props.classroomId}`, - params: { - token: this.props.classroomToken - } + uri: uri, + params: params }, (err, body, res) => { this.setState({waiting: false}); if (err) { @@ -164,14 +175,23 @@ StudentRegistration.defaultProps = { const IntlStudentRegistration = injectIntl(StudentRegistration); -const [classroomId, _, classroomToken] = document.location.pathname.split('/').filter(p => { - if (p) { - return p; +// parse either format of student registration url: +// "class register": http://scratch.mit.edu/classes/3/register/c0256654e1be +// "join token": http://scratch.mit.edu/join/c025r54ebe +let classroomId = null; +let classroomToken = null; +const classRegisterRegexp = /^\/?classes\/(\d*)\/register\/([a-zA-Z0-9]*)\/?$/; +const classRegisterMatch = classRegisterRegexp.exec(document.location.pathname); +if (classRegisterMatch) { + classroomId = classRegisterMatch[1]; + classroomToken = classRegisterMatch[2]; +} else { + const joinTokenRegexp = /^\/?join\/([a-zA-Z0-9]*)\/?$/; + const joinTokenMatch = joinTokenRegexp.exec(document.location.pathname); + if (joinTokenMatch) { + classroomToken = joinTokenMatch[1]; } - return null; -}) - .slice(-3); - +} const props = {classroomId, classroomToken}; render(, document.getElementById('app')); From 81678b70a75d1c8a85bb3692bc07853ac7374e14 Mon Sep 17 00:00:00 2001 From: Ben Wheeler Date: Fri, 3 Apr 2020 16:11:45 -0400 Subject: [PATCH 03/20] refactored uri pathname parsing to library --- src/lib/route.js | 15 ++++++++ .../studentregistration.jsx | 34 +++---------------- test/unit/lib/route.test.js | 26 ++++++++++++++ 3 files changed, 45 insertions(+), 30 deletions(-) create mode 100644 src/lib/route.js create mode 100644 test/unit/lib/route.test.js diff --git a/src/lib/route.js b/src/lib/route.js new file mode 100644 index 000000000..4eee7620d --- /dev/null +++ b/src/lib/route.js @@ -0,0 +1,15 @@ +module.exports = {}; + +// try to extract classroom token from either of two routes +module.exports.getURIClassroomToken = uriPathname => { + // first try to match /classes/CLASSROOM_ID/register/CLASSROOM_TOKEN + const classRegisterRegexp = /^\/?classes\/\d*\/register\/([a-zA-Z0-9]*)\/?$/; + const classRegisterMatch = classRegisterRegexp.exec(uriPathname); + if (classRegisterMatch) return classRegisterMatch[1]; + // if regex match failed, try to match /join/CLASSROOM_TOKEN + const joinTokenRegexp = /^\/?join\/([a-zA-Z0-9]*)\/?$/; + const joinTokenMatch = joinTokenRegexp.exec(uriPathname); + if (joinTokenMatch) return joinTokenMatch[1]; + // if neither matched + return null; +}; diff --git a/src/views/studentregistration/studentregistration.jsx b/src/views/studentregistration/studentregistration.jsx index 78443686b..35bfee2ed 100644 --- a/src/views/studentregistration/studentregistration.jsx +++ b/src/views/studentregistration/studentregistration.jsx @@ -6,6 +6,7 @@ const React = require('react'); const api = require('../../lib/api'); const injectIntl = require('../../lib/intl.jsx').injectIntl; const intlShape = require('../../lib/intl.jsx').intlShape; +const route = require('../../lib/route'); const Deck = require('../../components/deck/deck.jsx'); const Progression = require('../../components/progression/progression.jsx'); @@ -33,21 +34,8 @@ class StudentRegistration extends React.Component { componentDidMount () { this.setState({waiting: true}); // eslint-disable-line react/no-did-mount-set-state - // set uri and params - let uri; - let params; - if (this.props.classroomId === null || typeof this.props.classroomId === 'undefined') { - // configure for token-only endpoint - uri = `/classtoken/${this.props.classroomToken}`; - } else { - // configure for endpoint expecting classroomId and token - uri = `/classrooms/${this.props.classroomId}`; - params = {token: this.props.classroomToken}; - } - api({ - uri: uri, - params: params + uri: `/classtoken/${this.props.classroomToken}` }, (err, body, res) => { this.setState({waiting: false}); if (err) { @@ -57,7 +45,7 @@ class StudentRegistration extends React.Component { }) }); } - if (res.statusCode === 404) { + if (res.statusCode >= 400) { // TODO: Use react-router for this window.location = '/404'; } @@ -178,20 +166,6 @@ const IntlStudentRegistration = injectIntl(StudentRegistration); // parse either format of student registration url: // "class register": http://scratch.mit.edu/classes/3/register/c0256654e1be // "join token": http://scratch.mit.edu/join/c025r54ebe -let classroomId = null; -let classroomToken = null; -const classRegisterRegexp = /^\/?classes\/(\d*)\/register\/([a-zA-Z0-9]*)\/?$/; -const classRegisterMatch = classRegisterRegexp.exec(document.location.pathname); -if (classRegisterMatch) { - classroomId = classRegisterMatch[1]; - classroomToken = classRegisterMatch[2]; -} else { - const joinTokenRegexp = /^\/?join\/([a-zA-Z0-9]*)\/?$/; - const joinTokenMatch = joinTokenRegexp.exec(document.location.pathname); - if (joinTokenMatch) { - classroomToken = joinTokenMatch[1]; - } -} -const props = {classroomId, classroomToken}; +const props = {classroomToken: route.getURIClassroomToken(document.location.pathname)}; render(, document.getElementById('app')); diff --git a/test/unit/lib/route.test.js b/test/unit/lib/route.test.js new file mode 100644 index 000000000..2acb17599 --- /dev/null +++ b/test/unit/lib/route.test.js @@ -0,0 +1,26 @@ +const route = require('../../../src/lib/route'); + +describe('unit test lib/route.js', () => { + + test('getURIClassroomToken exists', () => { + expect(typeof route.getURIClassroomToken).toBe('function'); + }); + + test('getURIClassroomToken parses URI paths like /classes/21/register/r9n5f5xk', () => { + let response; + response = route.getURIClassroomToken('/classes/21/register/r9n5f5xk'); + expect(response).toEqual('r9n5f5xk'); + }); + + test('getURIClassroomToken parses URI paths like /join/e2dcfkx95', () => { + let response; + response = route.getURIClassroomToken('/join/e2dcfkx95'); + expect(response).toEqual('e2dcfkx95'); + }); + + test('getURIClassroomToken works with trailing slash', () => { + let response; + response = route.getURIClassroomToken('/join/r9n5f5xk/'); + expect(response).toEqual('r9n5f5xk'); + }); +}); From 2972c528f4ccfcea32a22febb74b043ea60162a6 Mon Sep 17 00:00:00 2001 From: Ben Wheeler Date: Mon, 6 Apr 2020 10:36:06 -0400 Subject: [PATCH 04/20] use state.classroom.id instead of url param classroomId --- src/views/studentregistration/studentregistration.jsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/views/studentregistration/studentregistration.jsx b/src/views/studentregistration/studentregistration.jsx index 35bfee2ed..6605d6145 100644 --- a/src/views/studentregistration/studentregistration.jsx +++ b/src/views/studentregistration/studentregistration.jsx @@ -75,7 +75,7 @@ class StudentRegistration extends React.Component { ), country: formData.user.country, is_robot: formData.user.isRobot, - classroom_id: this.props.classroomId, + classroom_id: this.state.classroom.id, classroom_token: this.props.classroomToken } }, (err, body, res) => { @@ -100,7 +100,7 @@ class StudentRegistration extends React.Component { }); } handleGoToClass () { - window.location = `/classes/${this.props.classroomId}/`; + window.location = `/classes/${this.state.classroom.id}/`; } render () { const usernameDescription = this.props.intl.formatMessage({id: 'registration.studentUsernameStepDescription'}); @@ -151,13 +151,11 @@ class StudentRegistration extends React.Component { } StudentRegistration.propTypes = { - classroomId: PropTypes.string.isRequired, classroomToken: PropTypes.string.isRequired, intl: intlShape }; StudentRegistration.defaultProps = { - classroomId: null, classroomToken: null }; From 93d7946af73fd4d8c7abdcabdd4d6a89d47bddd8 Mon Sep 17 00:00:00 2001 From: Chris Garrity Date: Fri, 10 Apr 2020 12:08:07 -0400 Subject: [PATCH 05/20] Remove unnecessary userAgent field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Freshdesk form/widget collects Browser and OS meta data so there’s no need to duplicate it. --- src/components/helpform/helpform.jsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/helpform/helpform.jsx b/src/components/helpform/helpform.jsx index 4e92b3eee..cf91ae7ff 100644 --- a/src/components/helpform/helpform.jsx +++ b/src/components/helpform/helpform.jsx @@ -7,8 +7,6 @@ const HelpForm = props => { const prefix = 'https://mitscratch.freshdesk.com/widgets/feedback_widget/new?&widgetType=embedded&widgetView=yes&screenshot=No&searchArea=No'; const title = `formTitle=${props.title}`; const username = `helpdesk_ticket[custom_field][cf_scratch_name_40167]=${props.user.username || ''}`; - const agentText = encodeURIComponent(window.navigator.userAgent); - const browser = `helpdesk_ticket[custom_field][cf_browser_40167]=${agentText}`; const formSubject = `helpdesk_ticket[subject]=${props.subject}`; const formDescription = `helpdesk_ticket[description]=${props.body}`; return ( @@ -31,7 +29,7 @@ const HelpForm = props => { height="744px" id="freshwidget-embedded-form" scrolling="no" - src={`${prefix}&${title}&${username}&${browser}&${formSubject}&${formDescription}`} + src={`${prefix}&${title}&${username}&${formSubject}&${formDescription}`} title={} width="100%" /> From 7d9e6ea8b7e2fa78e25eb4609c04c2aaced65c12 Mon Sep 17 00:00:00 2001 From: Chris Garrity Date: Fri, 10 Apr 2020 12:16:44 -0400 Subject: [PATCH 06/20] Add pop up Help widget Initial version of contact us with the pop up Freshdesk help widget. * adding feature flag `?CONTACT_US_POPUP=true` to the URL allows to switch between current contact-us with form, and new contact-us with popup widget. * new copy for the contact us page (selected based on feature flag) * handles scratchr2 redirects by pre-opening the popup on the contact us form and handles multiple parameters --- src/components/helpwidget/helpwidget.jsx | 119 +++++++++++++++++++ src/l10n.json | 1 + src/lib/feature-flags.js | 3 +- src/views/contact-us/contact-us.jsx | 145 ++++++++++++++++------- src/views/contact-us/contact-us.scss | 8 ++ src/views/contact-us/l10n.json | 10 +- 6 files changed, 242 insertions(+), 44 deletions(-) create mode 100644 src/components/helpwidget/helpwidget.jsx create mode 100644 src/views/contact-us/contact-us.scss diff --git a/src/components/helpwidget/helpwidget.jsx b/src/components/helpwidget/helpwidget.jsx new file mode 100644 index 000000000..5ec894e8c --- /dev/null +++ b/src/components/helpwidget/helpwidget.jsx @@ -0,0 +1,119 @@ +const FormattedMessage = require('react-intl').FormattedMessage; +const injectIntl = require('react-intl').injectIntl; +const intlShape = require('react-intl').intlShape; +const bindAll = require('lodash.bindall'); +const connect = require('react-redux').connect; +const PropTypes = require('prop-types'); +const React = require('react'); + +const Button = require('../forms/button.jsx'); +/** + * Footer link that opens Freshdesk help widger + */ +class HelpWidget extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleOpenWidget', + 'openPopup' + ]); + } + componentDidMount () { + // don't add the script to the page more than once + if (document.getElementById('helpwidgetscript') === null) { + const script = document.createElement('script'); + script.id = 'helpwidgetscript'; + script.src = 'https://widget.freshworks.com/widgets/4000000089.js'; + script.async = true; + script.defer = true; + script.onload = () => this.scriptLoaded(); + + document.body.appendChild(script); + window.fwSettings = { + widget_id: 4000000089, + locale: this.props.intl.locale + }; + } + } + scriptLoaded () { + // freshdesk widget embed code + /* eslint-disable */ + !(function(){if("function"!=typeof window.FreshworksWidget){var n=function(){n.q.push(arguments)};n.q=[],window.FreshworksWidget=n}}()) + /* eslint-enable */ + // don't show the Freshdesk button + window.FreshworksWidget('hide', 'launcher'); + window.FreshworksWidget('setLabels', { + fr: { + banner: 'Bienvenue a Support', + contact_form: { + title: this.props.intl.formatMessage({id: 'contactUs.contactScratch'}) + } + } + }); + if (this.props.subject !== '') { + // open the popup already on the form if passed Inappropriate content params + this.openPopup(true); + } + } + handleOpenWidget (e) { + e.preventDefault(); + this.openPopup(); + } + openPopup (formOpen) { + if (typeof window.FreshworksWidget === 'function') { + window.FreshworksWidget('prefill', 'ticketForm', { + subject: this.props.subject, + description: this.props.body, + custom_fields: { + cf_scratch_name: this.props.user.username + } + }); + if (formOpen) { + window.FreshworksWidget('open', 'ticketForm'); + } else { + window.FreshworksWidget('open'); + } + } + } + render () { + return ( + + {this.props.button ? ( + + ) : () + } + + ); + } +} + +HelpWidget.propTypes = { + body: PropTypes.string, + button: PropTypes.bool, + intl: intlShape, + subject: PropTypes.string, + user: PropTypes.shape({ + classroomId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + thumbnailUrl: PropTypes.string, + username: PropTypes.string + }) +}; + +HelpWidget.defaultProps = { + body: '', + button: false, + subject: '', + user: {username: ''} +}; + +const mapStateToProps = state => ({ + user: state.session.session.user +}); + +const ConnectedHelpWidget = connect(mapStateToProps)(HelpWidget); +module.exports = injectIntl(ConnectedHelpWidget); diff --git a/src/l10n.json b/src/l10n.json index ab2e30ee6..960f6881d 100644 --- a/src/l10n.json +++ b/src/l10n.json @@ -12,6 +12,7 @@ "general.community": "Community", "general.confirmEmail": "Confirm Email", "general.contactUs": "Contact Us", + "general.getHelp": "Get Help", "general.contact": "Contact", "general.done": "Done", "general.downloadPDF": "Download PDF", diff --git a/src/lib/feature-flags.js b/src/lib/feature-flags.js index 009c37464..3bcf9255c 100644 --- a/src/lib/feature-flags.js +++ b/src/lib/feature-flags.js @@ -8,5 +8,6 @@ const flagInUrl = flag => { }; module.exports = { - CHROME_APP_RELEASED: true + CHROME_APP_RELEASED: true, + CONTACT_US_POPUP: isStaging() && flagInUrl('CONTACT_US_POPUP') }; diff --git a/src/views/contact-us/contact-us.jsx b/src/views/contact-us/contact-us.jsx index 613d99418..3f042bcad 100644 --- a/src/views/contact-us/contact-us.jsx +++ b/src/views/contact-us/contact-us.jsx @@ -7,8 +7,11 @@ const Page = require('../../components/page/www/page.jsx'); const render = require('../../lib/render.jsx'); const HelpForm = require('../../components/helpform/helpform.jsx'); +const HelpWidget = require('../../components/helpwidget/helpwidget.jsx'); +const {CONTACT_US_POPUP} = require('../../lib/feature-flags.js'); const InformationPage = require('../../components/informationpage/informationpage.jsx'); +require('./contact-us.scss'); class ContactUs extends React.Component { constructor (props) { @@ -18,65 +21,123 @@ class ContactUs extends React.Component { body: '' }; const query = window.location.search; - // assumes that scratchr2 will only ever send one parameter + let scratchId = ''; // The subject is not localized because sending in English is easier for Scratch Team if (query.indexOf('studio=') !== -1) { - this.state.subject = `Inappropriate content reported in studio ${query.split('=')[1]}`; - this.state.body = `https://scratch.mit.edu/studios/${query.split('=')[1]}`; + scratchId = query.match(/studio=([0-9]+)/)[1]; + this.state.subject = `Inappropriate content reported in studio ${scratchId}`; + this.state.body = `https://scratch.mit.edu/studios/${scratchId}`; } else if (query.indexOf('profile=') !== -1) { - this.state.subject = `Inappropriate content reported in profile ${query.split('=')[1]}`; - this.state.body = `https://scratch.mit.edu/users/${query.split('=')[1]}`; + scratchId = query.match(/profile=([a-zA-Z0-9-_]+)/)[1]; + this.state.subject = `Inappropriate content reported in profile ${scratchId}`; + this.state.body = `https://scratch.mit.edu/users/${scratchId}`; } else if (query.indexOf('confirmation=') !== -1) { this.state.subject = 'Problem with email confirmation'; } } render () { return ( - -
-
- -

- )}} - />

-

-
    -
  • + + {!CONTACT_US_POPUP && ( +
    +
    + +

    )}} - />

  • -
  • + />

    +

    +
      +
    • + )}} + />
    • +
    • + )}} + />
    • +
    • + )}} + />
    • +
    +

    +
+
+ )} + {CONTACT_US_POPUP && ( +
+
+ +

+ +

+

)}} - /> -

  • - )}} - />
  • - -

    -
    -
    + />

    +

    + +

    +

    +

    +
      +
    • +
    • +
    • +
    +

    + +

    +

    + + )}} + /> +

    + + + + )} - + {!CONTACT_US_POPUP && ( + + )}
    ); } diff --git a/src/views/contact-us/contact-us.scss b/src/views/contact-us/contact-us.scss new file mode 100644 index 000000000..9076f95a5 --- /dev/null +++ b/src/views/contact-us/contact-us.scss @@ -0,0 +1,8 @@ +#contact-us.helpwidget { + margin-bottom: 0; +} +.contact-us { + .gethelp-button { + margin-bottom: 2rem; + } +} diff --git a/src/views/contact-us/l10n.json b/src/views/contact-us/l10n.json index 85054b2df..0a1828b89 100644 --- a/src/views/contact-us/l10n.json +++ b/src/views/contact-us/l10n.json @@ -12,5 +12,13 @@ "contactUs.bugsLinkText":"Bugs and Glitches", "contactUs.formIntro":"If you still need to contact us, please fill out the form below with as much detail as you can. If you have any screenshots, attachments or links that help to explain your problem, please include them. We get a lot of mail, so we may not be able to respond to your message.", "contactUs.findHelp":"Where to find help:", - "contactUs.contactScratch":"Contact the Scratch Team" + "contactUs.contactScratch":"Contact the Scratch Team", + "contactUs.qTitle":"Questions", + "contactUs.seeFaq":"See the FAQ", + "contactUs.faqInfo":"You can find a list of answers to many questions about Scratch on our {faqLink} page.", + "contactUs.askCommunity":"Ask the Community", + "contactUs.forumsIntro":"You can also look through and post questions in the Scratch Discussion forums.", + "contactUs.forumsHelp":"There are many friendly and experienced Scratch community members who can help with the following topics and more:", + "contactUs.needSupport":"Need Support?", + "contactUs.supportInfo":"Click {helpLink} to type in a question about anything related to Scratch or to contact us. The Scratch Team receives lots of messages each day and is not able to answer each one individually, so we encourage you to read our online support articles and participate in the Discussion forums." } From 23306ff336fcd19cfcd6ece025e0d7ce7fd20388 Mon Sep 17 00:00:00 2001 From: Chris Garrity Date: Fri, 10 Apr 2020 16:09:17 -0400 Subject: [PATCH 07/20] remove excess space in the form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There’s too much space after removing the redundant field. --- src/components/helpform/helpform.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/helpform/helpform.jsx b/src/components/helpform/helpform.jsx index cf91ae7ff..81047786c 100644 --- a/src/components/helpform/helpform.jsx +++ b/src/components/helpform/helpform.jsx @@ -26,7 +26,7 @@ const HelpForm = props => {