1
1
import { getConfig } from '@edx/frontend-platform' ;
2
+ import { sendTrackEvent } from '@edx/frontend-platform/analytics' ;
2
3
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' ;
4
7
5
8
import { StrictDict , useKeyedState } from '@edx/react-unit-test-utils' ;
6
9
import { logError } from '@edx/frontend-platform/logging' ;
7
10
8
11
import { fetchCourse } from '@src/courseware/data' ;
9
12
import { processEvent } from '@src/course-home/data/thunks' ;
10
13
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' ;
11
17
import { messageTypes } from '../constants' ;
12
18
13
19
import useLoadBearingHook from './useLoadBearingHook' ;
@@ -17,6 +23,7 @@ export const stateKeys = StrictDict({
17
23
hasLoaded : 'hasLoaded' ,
18
24
showError : 'showError' ,
19
25
windowTopOffset : 'windowTopOffset' ,
26
+ sequences : 'sequences' ,
20
27
} ) ;
21
28
22
29
const useIFrameBehavior = ( {
@@ -29,6 +36,12 @@ const useIFrameBehavior = ({
29
36
useLoadBearingHook ( id ) ;
30
37
31
38
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 ) ;
32
45
33
46
const [ iframeHeight , setIframeHeight ] = useKeyedState ( stateKeys . iframeHeight , 0 ) ;
34
47
const [ hasLoaded , setHasLoaded ] = useKeyedState ( stateKeys . hasLoaded , false ) ;
@@ -70,6 +83,12 @@ const useIFrameBehavior = ({
70
83
// We listen for this message from LMS to know when the page needs to
71
84
// be scrolled to another location on the page.
72
85
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
+ }
73
92
}
74
93
} , [
75
94
id ,
@@ -84,6 +103,49 @@ const useIFrameBehavior = ({
84
103
85
104
useEventListener ( 'message' , receiveMessage ) ;
86
105
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
+
87
149
/**
88
150
* onLoad *should* only fire after everything in the iframe has finished its own load events.
89
151
* Which means that the plugin.resize message (which calls setHasLoaded above) will have fired already
@@ -94,6 +156,10 @@ const useIFrameBehavior = ({
94
156
const handleIFrameLoad = ( ) => {
95
157
if ( ! hasLoaded ) {
96
158
setShowError ( true ) ;
159
+ sendTrackEvent ( 'edx.bi.error.learning.iframe_load_failed' , {
160
+ iframeUrl,
161
+ unitId : id ,
162
+ } ) ;
97
163
logError ( 'Unit iframe failed to load. Server possibly returned 4xx or 5xx response.' , {
98
164
iframeUrl,
99
165
} ) ;
@@ -105,6 +171,11 @@ const useIFrameBehavior = ({
105
171
} ;
106
172
} ;
107
173
174
+ React . useEffect ( ( ) => {
175
+ setIframeHeight ( 0 ) ;
176
+ setHasLoaded ( false ) ;
177
+ } , [ iframeUrl ] ) ;
178
+
108
179
return {
109
180
iframeHeight,
110
181
handleIFrameLoad,
0 commit comments