Skip to content

Commit 4cca633

Browse files
committed
fix: Fixes the auto_advance feature for video XBlock
The commit adds eventlistener which picks up the autoAdvance message and triggers the next sequence. This has the same effect of clicking the next button. Signed-off-by: Farhaan Bukhsh <[email protected]>
1 parent 8356505 commit 4cca633

File tree

3 files changed

+80
-1
lines changed

3 files changed

+80
-1
lines changed

src/courseware/course/sequence/Unit/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const messageTypes = StrictDict({
1616
modal: 'plugin.modal',
1717
resize: 'plugin.resize',
1818
videoFullScreen: 'plugin.videoFullScreen',
19+
autoAdvance: 'plugin.autoAdvance',
1920
});
2021

2122
export default StrictDict({

src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.js

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import { getConfig } from '@edx/frontend-platform';
2+
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
23
import React from 'react';
3-
import { useDispatch } from 'react-redux';
4+
import { useDispatch, useSelector } from 'react-redux';
5+
import { useNavigate } from 'react-router-dom';
6+
import { throttle } from 'lodash';
47

58
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
69
import { logError } from '@edx/frontend-platform/logging';
710

811
import { fetchCourse } from '@src/courseware/data';
912
import { processEvent } from '@src/course-home/data/thunks';
1013
import { useEventListener } from '@src/generic/hooks';
14+
import { getSequenceId } from '@src/courseware/data/selectors';
15+
import { useModel } from '@src/generic/model-store';
16+
import { useSequenceNavigationMetadata } from '@src/courseware/course/sequence/sequence-navigation/hooks';
1117
import { messageTypes } from '../constants';
1218

1319
import useLoadBearingHook from './useLoadBearingHook';
@@ -17,6 +23,7 @@ export const stateKeys = StrictDict({
1723
hasLoaded: 'hasLoaded',
1824
showError: 'showError',
1925
windowTopOffset: 'windowTopOffset',
26+
sequences: 'sequences',
2027
});
2128

2229
const useIFrameBehavior = ({
@@ -29,6 +36,12 @@ const useIFrameBehavior = ({
2936
useLoadBearingHook(id);
3037

3138
const dispatch = useDispatch();
39+
const activeSequenceId = useSelector(getSequenceId);
40+
const navigate = useNavigate();
41+
const activeSequence = useModel(stateKeys.sequences, activeSequenceId);
42+
const activeUnitId = activeSequence.unitIds.length > 0
43+
? activeSequence.unitIds[activeSequence.activeUnitIndex] : null;
44+
const { isLastUnit, nextLink } = useSequenceNavigationMetadata(activeSequenceId, activeUnitId);
3245

3346
const [iframeHeight, setIframeHeight] = useKeyedState(stateKeys.iframeHeight, 0);
3447
const [hasLoaded, setHasLoaded] = useKeyedState(stateKeys.hasLoaded, false);
@@ -70,6 +83,12 @@ const useIFrameBehavior = ({
7083
// We listen for this message from LMS to know when the page needs to
7184
// be scrolled to another location on the page.
7285
window.scrollTo(0, data.offset + document.getElementById('unit-iframe').offsetTop);
86+
} else if (type === messageTypes.autoAdvance) {
87+
// We are listening to autoAdvance message to move to next sequence automatically.
88+
// In case it is the last unit we need not do anything.
89+
if (!isLastUnit) {
90+
navigate(nextLink);
91+
}
7392
}
7493
}, [
7594
id,
@@ -84,6 +103,49 @@ const useIFrameBehavior = ({
84103

85104
useEventListener('message', receiveMessage);
86105

106+
// Send visibility status to the iframe. It's used to mark XBlocks as viewed.
107+
React.useEffect(() => {
108+
if (!hasLoaded) {
109+
return undefined;
110+
}
111+
112+
const iframeElement = document.getElementById(elementId);
113+
if (!iframeElement || !iframeElement.contentWindow) {
114+
return undefined;
115+
}
116+
117+
const updateIframeVisibility = () => {
118+
const rect = iframeElement.getBoundingClientRect();
119+
const visibleInfo = {
120+
type: 'unit.visibilityStatus',
121+
data: {
122+
topPosition: rect.top,
123+
viewportHeight: window.innerHeight,
124+
},
125+
};
126+
iframeElement.contentWindow.postMessage(
127+
visibleInfo,
128+
`${getConfig().LMS_BASE_URL}`,
129+
);
130+
};
131+
132+
// Throttle the update function to prevent it from sending too many messages to the iframe.
133+
const throttledUpdateVisibility = throttle(updateIframeVisibility, 100);
134+
135+
// Update the visibility of the iframe in case the element is already visible.
136+
updateIframeVisibility();
137+
138+
// Add event listeners to update the visibility of the iframe when the window is scrolled or resized.
139+
window.addEventListener('scroll', throttledUpdateVisibility);
140+
window.addEventListener('resize', throttledUpdateVisibility);
141+
142+
// Clean up event listeners on unmount.
143+
return () => {
144+
window.removeEventListener('scroll', throttledUpdateVisibility);
145+
window.removeEventListener('resize', throttledUpdateVisibility);
146+
};
147+
}, [hasLoaded, elementId]);
148+
87149
/**
88150
* onLoad *should* only fire after everything in the iframe has finished its own load events.
89151
* Which means that the plugin.resize message (which calls setHasLoaded above) will have fired already
@@ -94,6 +156,10 @@ const useIFrameBehavior = ({
94156
const handleIFrameLoad = () => {
95157
if (!hasLoaded) {
96158
setShowError(true);
159+
sendTrackEvent('edx.bi.error.learning.iframe_load_failed', {
160+
iframeUrl,
161+
unitId: id,
162+
});
97163
logError('Unit iframe failed to load. Server possibly returned 4xx or 5xx response.', {
98164
iframeUrl,
99165
});
@@ -105,6 +171,11 @@ const useIFrameBehavior = ({
105171
};
106172
};
107173

174+
React.useEffect(() => {
175+
setIframeHeight(0);
176+
setHasLoaded(false);
177+
}, [iframeUrl]);
178+
108179
return {
109180
iframeHeight,
110181
handleIFrameLoad,

src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jest.mock('react', () => ({
2525

2626
jest.mock('react-redux', () => ({
2727
useDispatch: jest.fn(),
28+
useSelector: jest.fn(),
2829
}));
2930

3031
jest.mock('./useLoadBearingHook', () => jest.fn());
@@ -42,6 +43,12 @@ jest.mock('@src/course-home/data/thunks', () => ({
4243
jest.mock('@src/generic/hooks', () => ({
4344
useEventListener: jest.fn(),
4445
}));
46+
jest.mock('@src/generic/model-store', () => ({
47+
useModel: () => ({ unitIds: ['unit1', 'unit2'], entranceExamData: { entranceExamPassed: null } }),
48+
}));
49+
jest.mock('react-router-dom', () => ({
50+
useNavigate: jest.fn(),
51+
}));
4552

4653
const state = mockUseKeyedState(stateKeys);
4754

0 commit comments

Comments
 (0)