11import { getConfig } from '@edx/frontend-platform' ;
2+ import { sendTrackEvent } from '@edx/frontend-platform/analytics' ;
23import 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
58import { StrictDict , useKeyedState } from '@edx/react-unit-test-utils' ;
69import { logError } from '@edx/frontend-platform/logging' ;
710
811import { fetchCourse } from '@src/courseware/data' ;
912import { processEvent } from '@src/course-home/data/thunks' ;
1013import { 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' ;
1117import { messageTypes } from '../constants' ;
1218
1319import 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
2229const 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,
0 commit comments