1- import { createCustomEvent , sendMessage , originalWindowDispatchEvent } from '../utils.js'
1+ import { Messaging , TestTransportConfig , WebkitMessagingConfig } from '@duckduckgo/messaging'
2+ import { createCustomEvent , originalWindowDispatchEvent } from '../utils.js'
23import { logoImg , loadingImages , closeIcon } from './click-to-load/ctl-assets.js'
34import { getStyles , getConfig } from './click-to-load/ctl-config.js'
5+ import { ClickToLoadMessagingTransport } from './click-to-load/ctl-messaging-transport.js'
46import ContentFeature from '../content-feature.js'
57import { DDGCtlPlaceholderBlockedElement } from './click-to-load/components/ctl-placeholder-blocked.js'
68import { registerCustomElements } from './click-to-load/components'
@@ -25,6 +27,9 @@ const titleID = 'DuckDuckGoPrivacyEssentialsCTLElementTitle'
2527let config = null
2628let sharedStrings = null
2729let styles = null
30+ // Used to choose between extension/desktop flow or mobile apps flow.
31+ // Updated on ClickToLoad.init()
32+ let isMobileApp
2833
2934// TODO: Remove these redundant data structures and refactor the related code.
3035// There should be no need to have the entity configuration stored in two
@@ -49,9 +54,20 @@ const readyToDisplayPlaceholders = new Promise(resolve => {
4954let afterPageLoadResolver
5055const afterPageLoad = new Promise ( resolve => { afterPageLoadResolver = resolve } )
5156
52- // Used to choose between extension/desktop flow or mobile apps flow.
53- // Updated on ClickToLoad.init()
54- let isMobileApp
57+ // Messaging layer for Click to Load. The messaging instance is initialized in
58+ // ClickToLoad.init() and updated here to be used outside ClickToLoad class
59+ // we need a module scoped reference.
60+ /** @type {import("@duckduckgo/messaging").Messaging } */
61+ let _messagingModuleScope
62+ const ctl = {
63+ /**
64+ * @return {import("@duckduckgo/messaging").Messaging }
65+ */
66+ get messaging ( ) {
67+ if ( ! _messagingModuleScope ) throw new Error ( 'Messaging not initialized' )
68+ return _messagingModuleScope
69+ }
70+ }
5571
5672/*********************************************************
5773 * Widget Replacement logic
@@ -377,7 +393,9 @@ class DuckWidget {
377393 if ( this . replaceSettings . type === 'loginButton' ) {
378394 isLogin = true
379395 }
380- window . addEventListener ( 'ddg-ctp-unblockClickToLoadContent-complete' , ( ) => {
396+ const action = this . entity === 'Youtube' ? 'block-ctl-yt' : 'block-ctl-fb'
397+ // eslint-disable-next-line promise/prefer-await-to-then
398+ unblockClickToLoadContent ( { entity : this . entity , action, isLogin } ) . then ( ( ) => {
381399 const parent = replacementElement . parentNode
382400
383401 // The placeholder was removed from the DOM while we loaded
@@ -455,9 +473,7 @@ class DuckWidget {
455473 if ( onError ) {
456474 fbElement . addEventListener ( 'error' , onError , { once : true } )
457475 }
458- } , { once : true } )
459- const action = this . entity === 'Youtube' ? 'block-ctl-yt' : 'block-ctl-fb'
460- unblockClickToLoadContent ( { entity : this . entity , action, isLogin } )
476+ } )
461477 }
462478 }
463479 // If this is a login button, show modal if needed
@@ -617,14 +633,14 @@ function createPlaceholderElementAndReplace (widget, trackingElement) {
617633
618634 // YouTube
619635 if ( widget . replaceSettings . type === 'youtube-video' ) {
620- sendMessage ( 'updateYouTubeCTLAddedFlag' , true )
636+ ctl . messaging . notify ( 'updateYouTubeCTLAddedFlag' , { youTubeCTLAddedFlag : true } )
621637 replaceYouTubeCTL ( trackingElement , widget )
622638
623639 // Subscribe to changes to youtubePreviewsEnabled setting
624640 // and update the CTL state
625- window . addEventListener (
626- 'ddg-settings-youtubePreviewsEnabled ' ,
627- ( /** @type CustomEvent */ { detail : value } ) => {
641+ ctl . messaging . subscribe (
642+ 'setYoutubePreviewsEnabled ' ,
643+ ( { value } ) => {
628644 isYoutubePreviewsEnabled = value
629645 replaceYouTubeCTL ( trackingElement , widget )
630646 }
@@ -678,7 +694,7 @@ function replaceYouTubeCTL (trackingElement, widget) {
678694 dataKey : 'yt-preview-toggle' , // data-key attribute for button
679695 label : widget . replaceSettings . previewToggleText , // Text to be presented with toggle
680696 size : isMobileApp ? 'lg' : 'md' ,
681- onClick : ( ) => sendMessage ( 'setYoutubePreviewsEnabled' , true ) // Toggle click callback
697+ onClick : ( ) => ctl . messaging . notify ( 'setYoutubePreviewsEnabled' , { youtubePreviewsEnabled : true } ) // Toggle click callback
682698 } ,
683699 withFeedback : {
684700 label : sharedStrings . shareFeedback ,
@@ -844,9 +860,10 @@ async function replaceClickToLoadElements (targetElement) {
844860 * the page.
845861 * @param {unblockClickToLoadContentRequest } message
846862 * @see {@link ddg-ctp-unblockClickToLoadContent-complete } for the response handler.
863+ * @returns {Promise<void> }
847864 */
848865function unblockClickToLoadContent ( message ) {
849- sendMessage ( 'unblockClickToLoadContent' , message )
866+ return ctl . messaging . request ( 'unblockClickToLoadContent' , message )
850867}
851868
852869/**
@@ -855,9 +872,10 @@ function unblockClickToLoadContent (message) {
855872 * shown.
856873 * @param {string } entity
857874 */
858- function runLogin ( entity ) {
875+ async function runLogin ( entity ) {
859876 const action = entity === 'Youtube' ? 'block-ctl-yt' : 'block-ctl-fb'
860- unblockClickToLoadContent ( { entity, action, isLogin : true } )
877+ await unblockClickToLoadContent ( { entity, action, isLogin : true } )
878+ // Communicate with surrogate to run login
861879 originalWindowDispatchEvent (
862880 createCustomEvent ( 'ddg-ctp-run-login' , {
863881 detail : {
@@ -868,8 +886,8 @@ function runLogin (entity) {
868886}
869887
870888/**
871- * Close the login dialog and abort. Called after the user clicks to cancel
872- * after the warning dialog is shown.
889+ * Close the login dialog and communicate with the surrogate to abort.
890+ * Called after the user clicks to cancel after the warning dialog is shown.
873891 * @param {string } entity
874892 */
875893function cancelModal ( entity ) {
@@ -883,11 +901,7 @@ function cancelModal (entity) {
883901}
884902
885903function openShareFeedbackPage ( ) {
886- sendMessage ( 'openShareFeedbackPage' , '' )
887- }
888-
889- function getYouTubeVideoDetails ( videoURL ) {
890- sendMessage ( 'getYouTubeVideoDetails' , videoURL )
904+ ctl . messaging . notify ( 'openShareFeedbackPage' )
891905}
892906
893907/*********************************************************
@@ -1528,7 +1542,7 @@ function createYouTubeBlockingDialog (trackingElement, widget) {
15281542 )
15291543 previewToggle . addEventListener (
15301544 'click' ,
1531- ( ) => makeModal ( widget . entity , ( ) => sendMessage ( 'setYoutubePreviewsEnabled' , true ) , widget . entity )
1545+ ( ) => makeModal ( widget . entity , ( ) => ctl . messaging . notify ( 'setYoutubePreviewsEnabled' , { youtubePreviewsEnabled : true } ) , widget . entity )
15321546 )
15331547 bottomRow . appendChild ( previewToggle )
15341548
@@ -1645,7 +1659,7 @@ function createYouTubePreview (originalElement, widget) {
16451659 )
16461660 previewToggle . addEventListener (
16471661 'click' ,
1648- ( ) => sendMessage ( 'setYoutubePreviewsEnabled' , false )
1662+ ( ) => ctl . messaging . notify ( 'setYoutubePreviewsEnabled' , { youtubePreviewsEnabled : false } )
16491663 )
16501664
16511665 /** Preview Info Text */
@@ -1677,12 +1691,10 @@ function createYouTubePreview (originalElement, widget) {
16771691 // We use .then() instead of await here to show the placeholder right away
16781692 // while the YouTube endpoint takes it time to respond.
16791693 const videoURL = originalElement . src || originalElement . getAttribute ( 'data-src' )
1680- getYouTubeVideoDetails ( videoURL )
1681- window . addEventListener ( 'ddg-ctp-youTubeVideoDetails' ,
1682- ( /** @type {CustomEvent } */ {
1683- detail : { videoURL : videoURLResp , status, title, previewImage }
1684- } ) => {
1685- if ( videoURLResp !== videoURL ) { return }
1694+ ctl . messaging . request ( 'getYouTubeVideoDetails' , { videoURL } )
1695+ // eslint-disable-next-line promise/prefer-await-to-then
1696+ . then ( ( { videoURL : videoURLResp , status, title, previewImage } ) => {
1697+ if ( ! status || videoURLResp !== videoURL ) { return }
16861698 if ( status === 'success' ) {
16871699 titleElement . innerText = title
16881700 titleElement . title = title
@@ -1691,8 +1703,7 @@ function createYouTubePreview (originalElement, widget) {
16911703 }
16921704 widget . autoplay = true
16931705 }
1694- }
1695- )
1706+ } )
16961707
16971708 /** Share Feedback Link */
16981709 const feedbackRow = makeShareFeedbackRow ( )
@@ -1701,48 +1712,17 @@ function createYouTubePreview (originalElement, widget) {
17011712 return { youTubePreview, shadowRoot }
17021713}
17031714
1704- // Convention is that each function should be named the same as the sendMessage
1705- // method we are calling into eg. calling `sendMessage('getClickToLoadState')`
1706- // will result in a response routed to `updateHandlers.getClickToLoadState()`.
1707- const messageResponseHandlers = {
1708- getClickToLoadState ( response ) {
1709- devMode = response . devMode
1710- isYoutubePreviewsEnabled = response . youtubePreviewsEnabled
1711-
1712- // Mark the feature as ready, to allow placeholder replacements to
1713- // start.
1714- readyToDisplayPlaceholdersResolver ( )
1715- } ,
1716- setYoutubePreviewsEnabled ( response ) {
1717- if ( response ?. messageType && typeof response ?. value === 'boolean' ) {
1718- originalWindowDispatchEvent (
1719- createCustomEvent (
1720- response . messageType , { detail : response . value }
1721- )
1722- )
1723- }
1724- } ,
1725- getYouTubeVideoDetails ( response ) {
1726- if ( response ?. status && typeof response . videoURL === 'string' ) {
1727- originalWindowDispatchEvent (
1728- createCustomEvent (
1729- 'ddg-ctp-youTubeVideoDetails' ,
1730- { detail : response }
1731- )
1732- )
1733- }
1734- } ,
1735- unblockClickToLoadContent ( ) {
1736- originalWindowDispatchEvent (
1737- createCustomEvent ( 'ddg-ctp-unblockClickToLoadContent-complete' )
1738- )
1739- }
1740- }
1741-
1742- const knownMessageResponseType = Object . prototype . hasOwnProperty . bind ( messageResponseHandlers )
1743-
17441715export default class ClickToLoad extends ContentFeature {
17451716 async init ( args ) {
1717+ /**
1718+ * Bail if no messaging backend - this is a debugging feature to ensure we don't
1719+ * accidentally enabled this
1720+ */
1721+ if ( ! this . messaging ) {
1722+ throw new Error ( 'Cannot operate click to load without a messaging backend' )
1723+ }
1724+ _messagingModuleScope = this . messaging
1725+
17461726 const websiteOwner = args ?. site ?. parentEntity
17471727 const settings = args ?. featureSettings ?. clickToLoad || { }
17481728 const locale = args ?. locale || 'en'
@@ -1790,8 +1770,8 @@ export default class ClickToLoad extends ContentFeature {
17901770 entityData [ entity ] = currentEntityData
17911771 }
17921772
1793- // Listen for events from "surrogate" scripts.
1794- addEventListener ( 'ddg-ctp' , ( /** @type {CustomEvent } */ event ) => {
1773+ // Listen for window events from "surrogate" scripts.
1774+ window . addEventListener ( 'ddg-ctp' , ( /** @type {CustomEvent } */ event ) => {
17951775 if ( ! ( 'detail' in event ) ) return
17961776
17971777 const entity = event . detail ?. entity
@@ -1811,12 +1791,22 @@ export default class ClickToLoad extends ContentFeature {
18111791 }
18121792 }
18131793 } )
1794+ // Listen to message from Platform letting CTL know that we're ready to
1795+ // replace elements in the page
1796+ // eslint-disable-next-line promise/prefer-await-to-then
1797+ this . messaging . subscribe (
1798+ 'displayClickToLoadPlaceholders' ,
1799+ // TODO: Pass `message.options.ruleAction` through, that way only
1800+ // content corresponding to the entity for that ruleAction need to
1801+ // be replaced with a placeholder.
1802+ ( ) => replaceClickToLoadElements ( )
1803+ )
18141804
18151805 // Request the current state of Click to Load from the platform.
18161806 // Note: When the response is received, the response handler resolves
18171807 // the readyToDisplayPlaceholders Promise.
1818- sendMessage ( 'getClickToLoadState' )
1819- await readyToDisplayPlaceholders
1808+ const clickToLoadState = await this . messaging . request ( 'getClickToLoadState' )
1809+ this . onClickToLoadState ( clickToLoadState )
18201810
18211811 // Then wait for the page to finish loading, and resolve the
18221812 // afterPageLoad Promise.
@@ -1844,6 +1834,12 @@ export default class ClickToLoad extends ContentFeature {
18441834 } , 0 )
18451835 }
18461836
1837+ /**
1838+ * This is only called by the current integration between Android and Extension and is now
1839+ * used to connect only these Platforms responses with the temporary implementation of
1840+ * ClickToLoadMessagingTransport that wraps this communication.
1841+ * This can be removed once they have their own Messaging integration.
1842+ */
18471843 update ( message ) {
18481844 // TODO: Once all Click to Load messages include the feature property, drop
18491845 // messages that don't include the feature property too.
@@ -1852,20 +1848,49 @@ export default class ClickToLoad extends ContentFeature {
18521848 const messageType = message ?. messageType
18531849 if ( ! messageType ) return
18541850
1855- // Message responses.
1856- if ( messageType === 'response' ) {
1857- const messageResponseType = message ?. responseMessageType
1858- if ( messageResponseType && knownMessageResponseType ( messageResponseType ) ) {
1859- return messageResponseHandlers [ messageResponseType ] ( message . response )
1860- }
1851+ if ( ! this . _clickToLoadMessagingTransport ) {
1852+ throw new Error ( '_clickToLoadMessagingTransport not ready. Cannot operate click to load without a messaging backend' )
18611853 }
18621854
1863- // Other known update messages.
1864- if ( messageType === 'displayClickToLoadPlaceholders' ) {
1865- // TODO: Pass `message.options.ruleAction` through, that way only
1866- // content corresponding to the entity for that ruleAction need to
1867- // be replaced with a placeholder.
1868- return replaceClickToLoadElements ( )
1855+ // Send to Messaging layer the response or subscription message received
1856+ // from the Platform.
1857+ return this . _clickToLoadMessagingTransport . onResponse ( message )
1858+ }
1859+
1860+ /**
1861+ * Update Click to Load internal state
1862+ * @param {Object } state Click to Load state response from the Platform
1863+ * @param {boolean } state.devMode Developer or Production environment
1864+ * @param {boolean } state.youtubePreviewsEnabled YouTube Click to Load - YT Previews enabled flag
1865+ */
1866+ onClickToLoadState ( state ) {
1867+ devMode = state . devMode
1868+ isYoutubePreviewsEnabled = state . youtubePreviewsEnabled
1869+
1870+ // Mark the feature as ready, to allow placeholder
1871+ // replacements to start.
1872+ readyToDisplayPlaceholdersResolver ( )
1873+ }
1874+
1875+ // Messaging layer between Click to Load and the Platform
1876+ get messaging ( ) {
1877+ if ( this . _messaging ) return this . _messaging
1878+
1879+ if ( this . platform . name === 'android' || this . platform . name === 'extension' || this . platform . name === 'macos' ) {
1880+ this . _clickToLoadMessagingTransport = new ClickToLoadMessagingTransport ( )
1881+ const config = new TestTransportConfig ( this . _clickToLoadMessagingTransport )
1882+ this . _messaging = new Messaging ( this . messagingContext , config )
1883+ return this . _messaging
1884+ } else if ( this . platform . name === 'ios' ) {
1885+ const config = new WebkitMessagingConfig ( {
1886+ secret : '' ,
1887+ hasModernWebkitAPI : true ,
1888+ webkitMessageHandlerNames : [ 'contentScopeScripts' ]
1889+ } )
1890+ this . _messaging = new Messaging ( this . messagingContext , config )
1891+ return this . _messaging
1892+ } else {
1893+ throw new Error ( 'Messaging not supported yet on platform: ' + this . name )
18691894 }
18701895 }
18711896}
0 commit comments