diff --git a/src/courseware/course/Course.jsx b/src/courseware/course/Course.jsx
index 5c180928..b13cddf4 100644
--- a/src/courseware/course/Course.jsx
+++ b/src/courseware/course/Course.jsx
@@ -10,7 +10,7 @@ import useOfferAlert from '../../alerts/offer-alert';
import Sequence from './sequence';
-import { CelebrationModal, shouldCelebrateOnSectionLoad } from './celebration';
+import { FirstSectionCelebrationModal, shouldCelebrateOnSectionLoad } from './celebration';
import CourseBreadcrumbs from './CourseBreadcrumbs';
import CourseSock from './course-sock';
import ContentTools from './content-tools';
@@ -78,7 +78,7 @@ function Course({
previousSequenceHandler={previousSequenceHandler}
/>
{celebrationOpen && (
-
diff --git a/src/courseware/course/Course.test.jsx b/src/courseware/course/Course.test.jsx
index 2b0faca5..6496405a 100644
--- a/src/courseware/course/Course.test.jsx
+++ b/src/courseware/course/Course.test.jsx
@@ -71,9 +71,9 @@ describe('Course', () => {
handleNextSectionCelebration(sequenceId, sequenceId, testData.unitId);
render(, { store: testStore });
- const celebrationModal = screen.getByRole('dialog');
- expect(celebrationModal).toBeInTheDocument();
- expect(getByRole(celebrationModal, 'heading', { name: 'Congratulations!' })).toBeInTheDocument();
+ const FirstSectionCelebrationModal = screen.getByRole('dialog');
+ expect(FirstSectionCelebrationModal).toBeInTheDocument();
+ expect(getByRole(FirstSectionCelebrationModal, 'heading', { name: 'Congratulations!' })).toBeInTheDocument();
});
it('displays upgrade sock', async () => {
diff --git a/src/courseware/course/celebration/FirstDiscussionCelebrationModal.jsx b/src/courseware/course/celebration/FirstDiscussionCelebrationModal.jsx
new file mode 100644
index 00000000..e699073b
--- /dev/null
+++ b/src/courseware/course/celebration/FirstDiscussionCelebrationModal.jsx
@@ -0,0 +1,91 @@
+import React, { useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { Modal } from '@edx/paragon';
+import { layoutGenerator } from 'react-break';
+import { useDispatch } from 'react-redux';
+
+import ClapsMobile from './assets/claps_280x201.gif';
+import ClapsTablet from './assets/claps_456x328.gif';
+import messages from './messages';
+import { recordFirstDiscussionCelebration } from './utils';
+import { updateModel } from '../../../generic/model-store';
+
+function FirstDiscussionCelebrationModal({
+ courseId, firstDiscussionUserBucket, intl, open, ...rest
+}) {
+ const dispatch = useDispatch();
+ const layout = layoutGenerator({
+ mobile: 0,
+ tablet: 400,
+ });
+
+ const OnMobile = layout.is('mobile');
+ const OnAtLeastTablet = layout.isAtLeast('tablet');
+
+ useEffect(() => {
+ if (open) {
+ recordFirstDiscussionCelebration(courseId);
+ }
+ }, [open]);
+
+ let normativeDataBodyText;
+ // Bucket 2 corresponds to showing normative data in the body text
+ if (firstDiscussionUserBucket === 0) {
+ if (Math.random() > 0.5) {
+ normativeDataBodyText = (
{intl.formatMessage(messages.discussionBodyText1)}
);
+ } else {
+ normativeDataBodyText = ({intl.formatMessage(messages.discussionBodyText2)}
);
+ }
+ }
+
+ console.log('normativeDataBodyText:', normativeDataBodyText);
+
+ return (
+
+ {intl.formatMessage(messages.conversation)}
+
+
+
+
+
+
+ {normativeDataBodyText}
+ >
+ )}
+ closeText={intl.formatMessage(messages.keepItUp)}
+ onClose={() => {
+ // Update our local copy of course data from LMS
+ console.log('updating model');
+ dispatch(updateModel({
+ modelType: 'courses',
+ model: {
+ id: courseId,
+ celebrations: {
+ firstDiscussion: false,
+ },
+ },
+ }));
+ }}
+ open={open}
+ title={intl.formatMessage(messages.firstDiscussionPost)}
+ {...rest}
+ />
+ );
+}
+
+FirstDiscussionCelebrationModal.defaultProps = {
+ open: false,
+ firstDiscussionUserBucket: 2,
+};
+
+FirstDiscussionCelebrationModal.propTypes = {
+ courseId: PropTypes.string.isRequired,
+ firstDiscussionUserBucket: PropTypes.number,
+ intl: intlShape.isRequired,
+ open: PropTypes.bool,
+};
+
+export default injectIntl(FirstDiscussionCelebrationModal);
diff --git a/src/courseware/course/celebration/CelebrationModal.jsx b/src/courseware/course/celebration/FirstSectionCelebrationModal.jsx
similarity index 91%
rename from src/courseware/course/celebration/CelebrationModal.jsx
rename to src/courseware/course/celebration/FirstSectionCelebrationModal.jsx
index 927cbda7..5ce5737e 100644
--- a/src/courseware/course/celebration/CelebrationModal.jsx
+++ b/src/courseware/course/celebration/FirstSectionCelebrationModal.jsx
@@ -10,7 +10,7 @@ import messages from './messages';
import SocialIcons from '../../social-share/SocialIcons';
import { recordFirstSectionCelebration } from './utils';
-function CelebrationModal({
+function FirstSectionCelebrationModal({
courseId, intl, open, ...rest
}) {
const layout = layoutGenerator({
@@ -58,14 +58,14 @@ function CelebrationModal({
);
}
-CelebrationModal.defaultProps = {
+FirstSectionCelebrationModal.defaultProps = {
open: false,
};
-CelebrationModal.propTypes = {
+FirstSectionCelebrationModal.propTypes = {
courseId: PropTypes.string.isRequired,
intl: intlShape.isRequired,
open: PropTypes.bool,
};
-export default injectIntl(CelebrationModal);
+export default injectIntl(FirstSectionCelebrationModal);
diff --git a/src/courseware/course/celebration/data/api.js b/src/courseware/course/celebration/data/api.js
index 609ab5e7..9a9fd5cb 100644
--- a/src/courseware/course/celebration/data/api.js
+++ b/src/courseware/course/celebration/data/api.js
@@ -4,9 +4,7 @@ import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
// Does not block on answer
-export function postFirstSectionCelebrationComplete(courseId) {
+export function postCelebrationComplete(courseId, payload) {
const url = new URL(`${getConfig().LMS_BASE_URL}/api/courseware/celebration/${courseId}`);
- getAuthenticatedHttpClient().post(url.href, {
- first_section: false,
- });
+ getAuthenticatedHttpClient().post(url.href, payload);
}
diff --git a/src/courseware/course/celebration/index.js b/src/courseware/course/celebration/index.js
index 015f7d43..34bea7a9 100644
--- a/src/courseware/course/celebration/index.js
+++ b/src/courseware/course/celebration/index.js
@@ -1,2 +1,7 @@
-export { default as CelebrationModal } from './CelebrationModal';
-export { handleNextSectionCelebration, shouldCelebrateOnSectionLoad } from './utils';
+export { default as FirstDiscussionCelebrationModal } from './FirstDiscussionCelebrationModal';
+export { default as FirstSectionCelebrationModal } from './FirstSectionCelebrationModal';
+export {
+ handleNextSectionCelebration,
+ shouldCelebrateOnSectionLoad,
+ shouldCelebrateOnDiscussionPost,
+} from './utils';
diff --git a/src/courseware/course/celebration/messages.js b/src/courseware/course/celebration/messages.js
index 8fa81c65..c758d656 100644
--- a/src/courseware/course/celebration/messages.js
+++ b/src/courseware/course/celebration/messages.js
@@ -9,6 +9,18 @@ const messages = defineMessages({
id: 'learning.celebration.congrats',
defaultMessage: 'Congratulations!',
},
+ conversation: {
+ id: 'learning.celebration.conversation',
+ defaultMessage: 'Nice job being part of the conversation.',
+ },
+ discussionBodyText1: {
+ id: 'learning.celebration.discussionBodyText1',
+ defaultMessage: "Learners who participate in discussions are 10x more likely to complete their course than those who don't.",
+ },
+ discussionBodyText2: {
+ id: 'learning.celebration.discussionBodyText2',
+ defaultMessage: "Learners who participate in discussions complete 3x as much course content on average vs. those who don't.",
+ },
earned: {
id: 'learning.celebration.earned',
defaultMessage: 'You earned it!',
@@ -18,11 +30,20 @@ const messages = defineMessages({
defaultMessage: "I'm on my way to completing {title} online with {platform}!",
description: 'Subject when sharing course progress via email',
},
+ firstDiscussionPost: {
+ id: 'learning.celebration.firstDiscussionPost',
+ defaultMessage: 'First Discussion post!',
+ },
forward: {
id: 'learning.celebration.forward',
defaultMessage: 'Keep going',
description: 'Button to close celebration dialog and get back to course',
},
+ keepItUp: {
+ id: 'learning.celebration.keepItUp',
+ defaultMessage: 'Keep it up',
+ description: 'Button to close celebration dialog and get back to course',
+ },
share: {
id: 'learning.celebration.share',
defaultMessage: 'Take a moment to celebrate and share your progress.',
diff --git a/src/courseware/course/celebration/utils.jsx b/src/courseware/course/celebration/utils.jsx
index 9fdebf34..cac62eca 100644
--- a/src/courseware/course/celebration/utils.jsx
+++ b/src/courseware/course/celebration/utils.jsx
@@ -1,11 +1,11 @@
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
-import { postFirstSectionCelebrationComplete } from './data/api';
+import { postCelebrationComplete } from './data/api';
import { clearLocalStorage, getLocalStorage, setLocalStorage } from '../../../data/localStorage';
import { updateModel } from '../../../generic/model-store';
-const CELEBRATION_LOCAL_STORAGE_KEY = 'CelebrationModal.showOnSectionLoad';
+const CELEBRATION_LOCAL_STORAGE_KEY = 'FirstSectionCelebrationModal.showOnSectionLoad';
// Records clicks through the end of a section, so that we can know whether we should celebrate when we finish loading
function handleNextSectionCelebration(sequenceId, nextSequenceId, nextUnitId) {
@@ -16,18 +16,34 @@ function handleNextSectionCelebration(sequenceId, nextSequenceId, nextUnitId) {
});
}
-function recordFirstSectionCelebration(courseId) {
- // Tell the LMS
- postFirstSectionCelebrationComplete(courseId);
-
- // Tell our analytics
+function sendCelebrationSegmentEvent(courseId, eventName) {
const { administrator } = getAuthenticatedUser();
- sendTrackEvent('edx.ui.lms.celebration.first_section.opened', {
+ sendTrackEvent(eventName, {
course_id: courseId,
is_staff: administrator,
});
}
+function recordFirstSectionCelebration(courseId) {
+ // Tell the LMS
+ postCelebrationComplete(courseId, { first_section: false });
+
+ // Tell our analytics
+ sendCelebrationSegmentEvent(courseId, 'edx.ui.lms.celebration.first_section.opened');
+}
+
+function recordFirstDiscussionCelebration(courseId) {
+ /* postCelebrationComplete should start being used once the Discussion MFE exists
+ and we no longer record the first discussion post in the JS handler in edx-platform
+ See edx-platform/common/static/common/js/discussion/views/new_post_view.js and
+ edx-platform/cms/static/common/js/discussion/views/discussion_thread_view.js */
+ // Tell the LMS
+ // postCelebrationComplete(courseId, {first_discussion: false});
+
+ // Tell our analytics
+ sendCelebrationSegmentEvent(courseId, 'edx.ui.lms.celebration.first_discussion.opened');
+}
+
// Looks at local storage to see whether we just came from the end of a section.
// Note! This does have side effects (will clear some local storage and may start an api call).
function shouldCelebrateOnSectionLoad(courseId, sequenceId, unitId, celebrateFirstSection, dispatch) {
@@ -63,4 +79,16 @@ function shouldCelebrateOnSectionLoad(courseId, sequenceId, unitId, celebrateFir
return shouldCelebrate;
}
-export { handleNextSectionCelebration, recordFirstSectionCelebration, shouldCelebrateOnSectionLoad };
+function shouldCelebrateOnDiscussionPost(firstDiscussion, firstDiscussionUserBucket) {
+ // Bucket 0 === Control group which does not get the discussion celebration.
+ // That check can be removed when we stop using the flag as an experiment.
+ return firstDiscussion && firstDiscussionUserBucket === 0;
+}
+
+export {
+ handleNextSectionCelebration,
+ recordFirstDiscussionCelebration,
+ recordFirstSectionCelebration,
+ shouldCelebrateOnSectionLoad,
+ shouldCelebrateOnDiscussionPost,
+};
diff --git a/src/courseware/course/sequence/Unit.jsx b/src/courseware/course/sequence/Unit.jsx
index 5f03097e..2e8c8584 100644
--- a/src/courseware/course/sequence/Unit.jsx
+++ b/src/courseware/course/sequence/Unit.jsx
@@ -12,6 +12,7 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Modal } from '@edx/paragon';
import messages from './messages';
import BookmarkButton from '../bookmark/BookmarkButton';
+import { FirstDiscussionCelebrationModal, shouldCelebrateOnDiscussionPost } from '../celebration';
import { useModel } from '../../../generic/model-store';
import PageLoading from '../../../generic/PageLoading';
import { processEvent } from '../../../course-home/data/thunks';
@@ -65,15 +66,21 @@ function Unit({
const [iframeHeight, setIframeHeight] = useState(0);
const [hasLoaded, setHasLoaded] = useState(false);
+ const [discussionPosted, setDiscussionPosted] = useState(false);
const [modalOptions, setModalOptions] = useState({ open: false });
const unit = useModel('units', id);
const course = useModel('courses', courseId);
const {
+ celebrations: {
+ firstDiscussion,
+ firstDiscussionUserBucket,
+ },
contentTypeGatingEnabled,
} = course;
const dispatch = useDispatch();
+ const shouldCelebrateDiscussionPost = shouldCelebrateOnDiscussionPost(firstDiscussion, firstDiscussionUserBucket);
// Do not remove this hook. See function description.
useLoadBearingHook(id);
@@ -109,6 +116,9 @@ function Unit({
return () => global.removeEventListener('message', messageEventListenerRef.current);
}, [id, setIframeHeight, hasLoaded, iframeHeight, setHasLoaded, onLoaded]);
+ console.log('shouldCelebrateDiscussionPost:', shouldCelebrateDiscussionPost);
+ console.log('discussionPosted:', discussionPosted);
+
return (
{unit.title}
@@ -157,6 +167,13 @@ function Unit({
dialogClassName="modal-lg"
/>
)}
+ {discussionPosted && shouldCelebrateDiscussionPost && (
+
+ )}